Compare commits

..
Author SHA1 Message Date
Jikoo c9e17e4a12 Update dependencies, use more descriptive record 2021-11-28 20:18:01 -05:00
Jikoo 78908932c5 Progress on tab completion
Complete type, amount, and enchant options
  - Still missing player and chunk bucket completion
Update to Java 16 for main plugin
  - Min supported version 1.16 requires Java 16
Determine uniqueness based on identifier for pseudojson
2021-11-27 16:23:22 -05:00
Jikoo 5f1a613b48 Repackage and rename for clarity 2021-11-27 13:56:31 -05:00
Jikoo 18c3dbaee0 Merge branch 'master' into dev/rewrite-search 2021-11-27 07:09:00 -05:00
Jikoo 6775988347 Stash changes 2021-11-23 12:45:40 -05:00
Jikoo 2580797633 Merge branch 'master' into dev/rewrite-search
# Conflicts:
#	plugin/pom.xml
#	plugin/src/main/java/com/lishid/openinv/commands/SearchContainerCommand.java
#	plugin/src/main/java/com/lishid/openinv/commands/SearchEnchantCommand.java
#	plugin/src/main/java/com/lishid/openinv/commands/SearchInvCommand.java
#	pom.xml
2021-11-17 10:59:17 -05:00
Jikoo 28929fd61e Clean up parsing, prep for tab completion 2021-05-29 09:38:06 -04:00
Jikoo 2445e7f8b6 Disallow concurrent searches from a single user 2021-05-13 07:53:00 -04:00
Jikoo 0af6403570 Move pseudo-JSON to a separate class
This and generifying parsing will make tab completion much easier to write.
2021-04-29 16:45:12 -04:00
Jikoo 55018ca228 Fix last parameter, issues with async chunks 2021-04-24 11:37:59 -04:00
Jikoo 64bd918f32 Merge branch 'master' into dev/rewrite-search
# Conflicts:
#	README.MD
#	api/pom.xml
#	internal/v1_16_R3/pom.xml
#	plugin/pom.xml
#	pom.xml
2021-04-23 14:00:24 -04:00
Jikoo fb6a3a7bcf Simplify dependency management
Use dependencyManagement for versioning shared dependencies
Use pluginManagement for versioning and configuring plugins

Uberjar is still including classes it shouldn't, but that's not a new problem.
2021-04-21 11:07:15 -04:00
Jikoo 4d4aa86399 Simplify enchantment spec 2021-04-21 11:06:53 -04:00
Jikoo 1dce65cecb Drop PaperLib 2021-04-21 11:05:21 -04:00
Jikoo c1145ae111 Search progress
Change detail specs to JSON-esque expressions
Implement matching option parsing
Split search bucket specs into multiple arguments
2021-04-16 12:51:19 -04:00
Jikoo 19f029f17f Search progress
Allow multi-world searches
Search player crafting inventory if open
2021-04-13 20:36:41 -04:00
Jikoo 916a8c0c55 Start reworking search 2021-04-11 17:35:15 -04:00
151 changed files with 5495 additions and 8448 deletions

No files matched your search

-10
View File
@@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "monthly"
@@ -1,55 +0,0 @@
name: Auto-merge Dependabot PRs
on:
workflow_run:
workflows: [ "Pull Request" ]
types: [ completed ]
jobs:
merge-dependabot:
if: "github.actor == 'dependabot[bot]'
&& github.event.workflow_run.event == 'pull_request'
&& github.event.workflow_run.conclusion == 'success'"
runs-on: ubuntu-latest
steps:
# Note: this is directly from GitHub's example for using data from a triggering workflow:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
- name: 'Download artifact'
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "pr_number"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/pr_number.zip`, Buffer.from(download.data));
# This might be a useless use of cat, but I'm not sure what shell Actions is going to be running.
- name: Add Pull Number Variable
run: |-
unzip pr_number.zip
echo "PR_NUMBER=$(cat pr_number)" >> "$GITHUB_ENV"
- name: Approve
uses: hmarr/auto-approve-action@v4.0.0
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
pull-request-number: "${{ env.PR_NUMBER }}"
- name: Merge
uses: pascalgn/automerge-action@v0.16.3
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
MERGE_LABELS: "dependencies,java"
MERGE_METHOD: "squash"
PULL_REQUEST: "${{ env.PR_NUMBER }}"
+48 -16
View File
@@ -2,28 +2,31 @@ name: OpenInv CI
on:
push:
branches:
- 'master'
tags-ignore:
- '**'
paths-ignore:
- resource-pack/**
# Enable running CI via other Actions, i.e. for drafting releases and handling PRs.
workflow_call:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout Code
uses: actions/checkout@v2
- uses: actions/setup-java@v4
- name: Set Up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: '21'
cache: 'maven'
distribution: 'adopt'
java-version: '17'
# Install Spigot dependencies if necessary.
# Use cache to speed up build
- name: Cache Maven Repo
uses: actions/cache@v2
id: cache
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
# Install Spigot dependencies.
# This script uses Maven to check all required installations and ensure that they are present.
- name: Install Spigot Dependencies
run: . scripts/install_spigot_dependencies.sh
@@ -33,13 +36,42 @@ jobs:
# Upload artifacts
- name: Upload Distributable Jar
id: upload-final
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: dist
path: ./target/OpenInv.jar
- name: Upload API Jar
id: upload-api
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v2
with:
name: api
path: ./api/target/openinvapi*.jar
release:
name: Create Github Release
needs: [ build ]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set Release Variables
run: bash ./scripts/set_release_env.sh
- name: Download Artifacts
uses: actions/download-artifact@v2
- name: Create Release
id: create-release
uses: softprops/action-gh-release@v0.1.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: ${{ env.VERSIONED_NAME }}
body: ${{ env.GENERATED_CHANGELOG }}
draft: true
prerelease: false
files: ./dist/OpenInv.jar
-45
View File
@@ -1,45 +0,0 @@
name: Draft Github Release
on:
push:
tags:
- '**'
jobs:
run-ci:
uses: Jikoo/OpenInv/.github/workflows/ci.yml@master
draft-release:
needs: [ run-ci ]
runs-on: ubuntu-latest
steps:
# Fetch all history - used to assemble changelog.
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set Release Variables
run: bash ./scripts/set_release_env.sh
- name: Download build
uses: actions/download-artifact@v4
with:
name: dist
path: dist
- name: Package resource pack
run: |-
pushd resource-pack
zip -r ../dist/openinv-legibility-pack.zip .
popd
- name: Create Release
id: create-release
uses: softprops/action-gh-release@v2.0.6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: ${{ env.VERSIONED_NAME }}
body: ${{ env.GENERATED_CHANGELOG }}
draft: true
prerelease: false
files: ./dist/**
-22
View File
@@ -1,22 +0,0 @@
name: Pull Request
on:
pull_request:
jobs:
run-ci:
uses: Jikoo/OpenInv/.github/workflows/ci.yml@master
store-dependabot-pr-data:
if: "github.actor == 'dependabot[bot]' && github.event_name == 'pull_request'"
runs-on: ubuntu-latest
steps:
# Note: this is directly from GitHub's example for using data from a triggering workflow:
# https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
- name: Store Pull Number
run: |
mkdir -p ./pr
echo ${{ github.event.number }} > ./pr/pr_number
- uses: actions/upload-artifact@v4
with:
name: pr_number
path: pr/
@@ -9,12 +9,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fetch Github Release Asset
uses: dsaltares/fetch-gh-release-asset@1.1.2
uses: dsaltares/fetch-gh-release-asset@0.0.5
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ github.event.release.id }}
@@ -26,7 +26,7 @@ jobs:
- name: Create CurseForge Release
uses: itsmeow/curseforge-upload@v3
with:
token: "${{ secrets.CURSEFORGE_TOKEN }}"
token: ${{ secrets.CURSEFORGE_TOKEN }}
project_id: 31432
game_endpoint: minecraft
file_path: ./OpenInv.jar
-24
View File
@@ -1,24 +0,0 @@
name: Resource Pack CI
on:
push:
branches:
- 'master'
tags-ignore:
- '**'
paths:
- resource-pack/**
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build resource pack
id: upload-resource-pack
uses: actions/upload-artifact@v4
with:
name: openinv-legibility-pack
path: ./resource-pack/
compression-level: 9
+106 -18
View File
@@ -1,17 +1,16 @@
## About
OpenInv is a [Bukkit plugin](https://dev.bukkit.org/projects/openinv) which allows users to open and edit anyone's inventory or ender chest - online or not!
OpenInv is a [Bukkit plugin](https://dev.bukkit.org/bukkit-plugins/openinv/) which allows users to open and edit anyone's inventory or ender chest - online or not!
## Features
- **OpenInv**: Open anyone's inventory, even if they're offline.
- Read-only mode! Don't grant edit permission.
- Cross-world support! Allow access only from the same world.
- No duplicate slots! Only armor is accessible when opening self (if allowed at all)!
- Drop items as the player! Place items in the dropper slot in the bottom right. Can be disabled via permission!
- Allow any item in armor slots! Configurable via permission.
- Read-only mode! No edits allowed! Don't grant the permission `OpenInv.editinv`
- Cross-world support! Don't grant `OpenInv.crossworld`
- No self-opening! Don't grant `OpenInv.openself`
- Drop items as the player! Place items in the unused slots to the right of the armor to drop them
- **OpenEnder**: Open anyone's ender chest, even if they're offline.
- Allow access only to own ender chest! Don't grant permission to open others.
- Read-only mode! Don't grant edit permission.
- Cross-world support! Allow access only from the same world.
- Read-only mode! No edits allowed! Don't grant `OpenInv.editender`
- Cross-world support! Don't grant `OpenInv.crossworld`
- No opening others! Don't grant `OpenInv.openenderall`
- **SilentContainer**: Open containers without displaying an animation or making sound.
- **AnyContainer**: Open containers, even if blocked by ocelots or blocks.
@@ -19,7 +18,88 @@ OpenInv is a [Bukkit plugin](https://dev.bukkit.org/projects/openinv) which allo
See [the wiki](https://github.com/Jikoo/OpenInv/wiki/Commands).
## Permissions
See [the wiki](https://github.com/Jikoo/OpenInv/wiki/Permissions)
<table>
<tr>
<th>Node</th>
<th>Description</th>
</tr>
<tr>
<td>OpenInv.*</td>
<td>Gives permission to use all of OpenInv.</td>
</tr>
<tr>
<td>OpenInv.openinv</td>
<td>Required to use /openinv.</td>
</tr>
<tr>
<td>OpenInv.openself</td>
<td>Required to open own inventory.</td>
</tr>
<tr>
<td>OpenInv.editinv</td>
<td>Required to make changes to open inventories.</td>
</tr>
<tr>
<td>OpenInv.openonline</td>
<td>Allows users to open online players' inventories. For compatibility reasons this is granted by the nodes OpenInv.openinv and OpenInv.openender.</td>
</tr>
<tr>
<td>OpenInv.openoffline</td>
<td>Allows users to open offline players' inventories. For compatibility reasons this is granted by the nodes OpenInv.openinv and OpenInv.openender.</td>
</tr>
<tr>
<td>OpenInv.openender</td>
<td>Required to use /openender.</td>
</tr>
<tr>
<td>OpenInv.editender</td>
<td>Required to make changes to open ender chests.</td>
</tr>
<tr>
<td>OpenInv.openenderall</td>
<td>Allows users to open others' ender chests. Without it, users can only open their own.</td>
</tr>
<tr>
<td>OpenInv.exempt</td>
<td>Prevents the player's inventory being opened by others.</td>
</tr>
<tr>
<td>OpenInv.override</td>
<td>Allows bypassing of the exempt permission.</td>
</tr>
<tr>
<td>OpenInv.crossworld</td>
<td>Allows cross-world usage of /openinv and /openender.</td>
</tr>
<tr>
<td>OpenInv.search</td>
<td>Required to use /searchinv and /searchender.</td>
</tr>
<tr>
<td>OpenInv.searchenchant</td>
<td>Required to use /searchenchant.</td>
</tr>
<tr>
<td>OpenInv.anychest</td>
<td>Required to use /anychest.</td>
</tr>
<tr>
<td>OpenInv.any.default</td>
<td>Cause AnyContainer to be enabled by default.</td>
</tr>
<tr>
<td>OpenInv.silent</td>
<td>Required to use /silentcontainer.</td>
</tr>
<tr>
<td>OpenInv.silent.default</td>
<td>Cause SilentContainer to be enabled by default.</td>
</tr>
<tr>
<td>OpenInv.spectate</td>
<td>Allows users in spectate gamemode to edit inventories.</td>
</tr>
</table>
## For Developers
@@ -44,14 +124,22 @@ The OpenInv API is available via [JitPack](https://jitpack.io/).
```
### Compilation
To compile, the relevant Spigot jars must be installed in the local repository.
As OpenInv is compiled against Mojang's mappings, you must run BuildTools with the `--remapped` argument:
`java -jar BuildTools.jar --remapped --rev $serverVersion`
`$serverVersion` is the version of the server, i.e. `1.18.1`
To compile, the relevant Spigot jars must be installed in your local repository using the `install` plugin:
```shell
mvn install:install-file -Dpackaging=jar -Dfile=spigot-1.8-R0.1-SNAPSHOT.jar \
-DgroupId=org.spigotmc -DartifactId=spigot -Dversion=1.8-R0.1-SNAPSHOT
```
Note that BuildTools automatically installs produced files. If you use BuildTools to compile Spigot locally, you don't need to install it manually.
If you want to use Paper as a dependency, you can install it by executing PaperClip with the property `paperclip.install` set to true:
```shell
wget -O paperclip.jar https://papermc.io/api/v1/paper/1.16.5/latest/download
java -jar -Dpaperclip.install=true paperclip.jar
```
To compile just the API, execute Maven as usual:
`mvn clean package`
To compile for a single version, specify the NMS revision you are targeting: `mvn -pl <NMS module> -am clean install`
To compile for a set of versions, you'll need to use a profile. The only provided profile is `all`. Select a profile using the `-P` argument: `mvn clean package -am -P all`
To compile the full plugin, use the provided profile with `-P all`:
`mvn clean package -am -P all`
For more information, check out the [official Maven guide](http://maven.apache.org/guides/introduction/introduction-to-profiles.html).
The final file is `target/OpenInv.jar`
+2 -2
View File
@@ -1,5 +1,5 @@
<!--
~ Copyright (C) 2011-2022 lishid. All rights reserved.
~ Copyright (C) 2011-2021 lishid. All rights reserved.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
@@ -21,7 +21,7 @@
<parent>
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<version>5.0.0</version>
<version>4.1.9-SNAPSHOT</version>
</parent>
<artifactId>openinvapi</artifactId>
+230 -105
View File
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,166 +17,291 @@
package com.lishid.openinv;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.IInventoryAccess;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.util.InventoryAccess;
import com.lishid.openinv.util.StringMetric;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.UUID;
import java.util.logging.Logger;
/**
* Interface defining behavior for the OpenInv plugin.
*
* @author Jikoo
*/
public interface IOpenInv {
/**
* Check if the server version is supported by OpenInv.
*
* @return true if the server version is supported
*/
boolean isSupportedVersion();
/**
* Check the configuration value for whether OpenInv saves player data when unloading players. This is exclusively
* for users who do not allow editing of inventories, only viewing, and wish to prevent any possibility of bugs such
* as lishid#40. If true, OpenInv will not ever save any edits made to players.
* Check the configuration value for whether or not OpenInv saves player data when unloading
* players. This is exclusively for users who do not allow editing of inventories, only viewing,
* and wish to prevent any possibility of bugs such as lishid#40. If true, OpenInv will not ever
* save any edits made to players.
*
* @return false unless configured otherwise
*/
boolean disableSaving();
/**
* Check the configuration value for whether OpenInv allows offline access. If true, OpenInv will not load or allow
* modification of players while they are not online. This does not prevent other plugins from using existing loaded
* players who have gone offline.
* Gets the active ISilentContainer implementation.
*
* @return false unless configured otherwise
* @since 4.2.0
*/
boolean disableOfflineAccess();
/**
* Check the configuration value for whether OpenInv uses history for opening commands. If false, OpenInv will use
* the previous parameterized search when no parameters are provided.
*
* @return false unless configured otherwise
* @since 4.3.0
*/
boolean noArgsOpensSelf();
/**
* Get the active {@link IAnySilentContainer} implementation.
*
* @return the active implementation for the server version
* @return the ISilentContainer
* @throws IllegalStateException if the server version is unsupported
*/
@NotNull IAnySilentContainer getAnySilentContainer();
/**
* Get whether a user has AnyContainer mode enabled.
* Gets the active IInventoryAccess implementation.
*
* @param offline the user to obtain the state of
* @return true if AnyContainer mode is enabled
*/
boolean getAnyContainerStatus(@NotNull OfflinePlayer offline);
/**
* Set whether a user has AnyContainer mode enabled.
*
* @param offline the user to set the state of
* @param status the state of the mode
*/
void setAnyContainerStatus(@NotNull OfflinePlayer offline, boolean status);
/**
* Get whether a user has SilentContainer mode enabled.
*
* @param offline the user to obtain the state of
* @return true if SilentContainer mode is enabled
*/
boolean getSilentContainerStatus(@NotNull OfflinePlayer offline);
/**
* Set whether a user has SilentContainer mode enabled.
*
* @param offline the user to set the state of
* @param status the state of the mode
*/
void setSilentContainerStatus(@NotNull OfflinePlayer offline, boolean status);
/**
* Get an {@link ISpecialEnderChest} for a user.
*
* @param player the {@link Player} owning the inventory
* @param online whether the owner is currently online
* @return the created inventory
* @return the IInventoryAccess
* @throws IllegalStateException if the server version is unsupported
* @throws InstantiationException if there was an issue creating the inventory
*/
@Deprecated
default @NotNull IInventoryAccess getInventoryAccess() {
return new InventoryAccess();
}
/**
* Gets the provided player's AnyChest setting.
*
* @param player the OfflinePlayer
* @return true if AnyChest is enabled
* @throws IllegalStateException if the server version is unsupported
*/
boolean getPlayerAnyChestStatus(@NotNull OfflinePlayer player);
/**
* Gets a unique identifier by which the OfflinePlayer can be referenced. Using the value
* returned to look up a Player will generally be much faster for later implementations.
*
* @param offline the OfflinePlayer
* @return the identifier
* @throws IllegalStateException if the server version is unsupported
*/
default @NotNull String getPlayerID(@NotNull OfflinePlayer offline) {
return offline.getUniqueId().toString();
}
/**
* Gets a player's SilentChest setting.
*
* @param offline the OfflinePlayer
* @return true if SilentChest is enabled
* @throws IllegalStateException if the server version is unsupported
*/
boolean getPlayerSilentChestStatus(@NotNull OfflinePlayer offline);
/**
* Gets an ISpecialEnderChest for the given Player.
*
* @param player the Player
* @param online true if the Player is currently online
* @return the ISpecialEnderChest
* @throws IllegalStateException if the server version is unsupported
* @throws InstantiationException if the ISpecialEnderChest could not be instantiated
*/
@NotNull ISpecialEnderChest getSpecialEnderChest(@NotNull Player player, boolean online) throws InstantiationException;
/**
* Get an {@link ISpecialPlayerInventory} for a user.
* Gets an ISpecialPlayerInventory for the given Player.
*
* @param player the {@link Player} owning the inventory
* @param online whether the owner is currently online
* @return the created inventory
* @throws IllegalStateException if the server version is unsupported
* @throws InstantiationException if there was an issue creating the inventory
* @param player the Player
* @param online true if the Player is currently online
* @return the ISpecialPlayerInventory
* @throws IllegalStateException if the server version is unsupported
* @throws InstantiationException if the ISpecialPlayerInventory could not be instantiated
*/
@NotNull ISpecialPlayerInventory getSpecialInventory(@NotNull Player player, boolean online) throws InstantiationException;
/**
* Open an {@link ISpecialInventory} for a {@link Player}.
* Checks if the server version is supported by OpenInv.
*
* @param player the viewer
* @param inventory the inventory to open
* @return the resulting {@link InventoryView}
* @return true if the server version is supported
*/
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
boolean isSupportedVersion();
/**
* Check if a {@link Player} is currently loaded by OpenInv.
* Load a Player from an OfflinePlayer. May return null under some circumstances.
*
* @param playerUuid the {@link UUID} of the {@code Player}
* @return whether the {@code Player} is loaded
* @since 4.2.0
*/
boolean isPlayerLoaded(@NotNull UUID playerUuid);
/**
* Load a {@link Player} from an {@link OfflinePlayer}. If the user has not played before or the default world for
* the server is not loaded, this will return {@code null}.
*
* @param offline the {@code OfflinePlayer} to load a {@code Player} for
* @return the loaded {@code Player}
* @param offline the OfflinePlayer to load a Player for
* @return the Player, or null
* @throws IllegalStateException if the server version is unsupported
*/
@Nullable Player loadPlayer(@NotNull final OfflinePlayer offline);
/**
* Match an existing {@link OfflinePlayer}. If the name is a {@link UUID#toString() UUID string}, this will only
* return the user if they have actually played on the server before, unlike {@link Bukkit#getOfflinePlayer(UUID)}.
*
* <p>This method is potentially very heavily blocking. It should not ever be called on the
* Get an OfflinePlayer by name.
* <p>
* Note: This method is potentially very heavily blocking. It should not ever be called on the
* main thread, and if it is, a stack trace will be displayed alerting server owners to the
* call.
*
* @param name the string to match
* @return the user with the closest matching name
* @param name the name of the Player
* @return the OfflinePlayer with the closest matching name or null if no players have ever logged in
*/
@Nullable OfflinePlayer matchPlayer(@NotNull String name);
default @Nullable OfflinePlayer matchPlayer(@NotNull String name) {
// Warn if called on the main thread - if we resort to searching offline players, this may take several seconds.
if (Bukkit.getServer().isPrimaryThread()) {
this.getLogger().warning("Call to OpenInv#matchPlayer made on the main thread!");
this.getLogger().warning("This can cause the server to hang, potentially severely.");
this.getLogger().log(Level.WARNING, new Throwable("Current stack trace"), () -> "Current stack trace");
}
OfflinePlayer player;
try {
UUID uuid = UUID.fromString(name);
player = Bukkit.getOfflinePlayer(uuid);
// Ensure player is a real player, otherwise return null
if (player.hasPlayedBefore() || player.isOnline()) {
return player;
}
} catch (IllegalArgumentException ignored) {
// Not a UUID
}
player = Bukkit.getServer().getPlayerExact(name);
if (player != null) {
return player;
}
player = Bukkit.getServer().getOfflinePlayer(name);
if (player.hasPlayedBefore()) {
return player;
}
player = Bukkit.getServer().getPlayer(name);
if (player != null) {
return player;
}
float bestMatch = 0;
for (OfflinePlayer offline : Bukkit.getServer().getOfflinePlayers()) {
if (offline.getName() == null) {
// Loaded by UUID only, name has never been looked up.
continue;
}
float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName());
if (currentMatch == 1.0F) {
return offline;
}
if (currentMatch > bestMatch) {
bestMatch = currentMatch;
player = offline;
}
}
// Only null if no players have played ever, otherwise even the worst match will do.
return player;
}
/**
* Forcibly close inventories of and unload any cached data for a user.
* Open an ISpecialInventory for a Player.
*
* @param offline the {@link OfflinePlayer} to unload
* @param player the Player
* @param inventory the ISpecialInventory
* @return the InventoryView for the opened ISpecialInventory
*/
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
/**
* Check the configuration value for whether or not OpenInv displays a notification to the user
* when a container is activated with AnyChest.
*
* @return true unless configured otherwise
* @deprecated OpenInv uses action bar chat for notifications. Whether or not they show is based on language settings.
*/
@Deprecated
default boolean notifyAnyChest() {
return true;
}
/**
* Check the configuration value for whether or not OpenInv displays a notification to the user
* when a container is activated with SilentChest.
*
* @return true unless configured otherwise
* @deprecated OpenInv uses action bar chat for notifications. Whether or not they show is based on language settings.
*/
@Deprecated
default boolean notifySilentChest() {
return true;
}
/**
* Mark a Player as no longer in use by a Plugin to allow OpenInv to remove it from the cache
* when eligible.
*
* @param player the Player
* @param plugin the Plugin no longer holding a reference to the Player
* @throws IllegalStateException if the server version is unsupported
*/
void releasePlayer(@NotNull Player player, @NotNull Plugin plugin);
/**
* Mark a Player as in use by a Plugin to prevent it from being removed from the cache. Used to
* prevent issues with multiple copies of the same Player being loaded such as lishid#49.
* Changes made to loaded copies overwrite changes to the others when saved, leading to
* duplication bugs and more.
* <p>
* When finished with the Player object, be sure to call {@link #releasePlayer(Player, Plugin)}
* to prevent the cache from keeping it stored until the plugin is disabled.
* <p>
* When using a Player object from OpenInv, you must handle the Player coming online, replacing
* your Player reference with the Player from the PlayerJoinEvent. In addition, you must change
* any values in the Player to reflect any unsaved alterations to the existing Player which do
* not affect the inventory or ender chest contents.
* <p>
* OpenInv only saves player data when unloading a Player from the cache, and then only if
* {@link #disableSaving()} returns false. If you are making changes that OpenInv does not cause
* to persist when a Player logs in as noted above, it is suggested that you manually call
* {@link Player#saveData()} when releasing your reference to ensure your changes persist.
*
* @param player the Player
* @param plugin the Plugin holding the reference to the Player
* @throws IllegalStateException if the server version is unsupported
*/
void retainPlayer(@NotNull Player player, @NotNull Plugin plugin);
/**
* Sets a player's AnyChest setting.
*
* @param offline the OfflinePlayer
* @param status the status
* @throws IllegalStateException if the server version is unsupported
*/
void setPlayerAnyChestStatus(@NotNull OfflinePlayer offline, boolean status);
/**
* Sets a player's SilentChest setting.
*
* @param offline the OfflinePlayer
* @param status the status
* @throws IllegalStateException if the server version is unsupported
*/
void setPlayerSilentChestStatus(@NotNull OfflinePlayer offline, boolean status);
/**
* Forcibly unload a cached Player's data.
*
* @param offline the OfflinePlayer to unload
* @throws IllegalStateException if the server version is unsupported
*/
void unload(@NotNull OfflinePlayer offline);
@@ -1,43 +0,0 @@
package com.lishid.openinv.event;
import com.google.errorprone.annotations.RestrictedApi;
import com.lishid.openinv.internal.ISpecialInventory;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Event fired before OpenInv saves a player's data when closing an {@link ISpecialInventory}.
*/
public class OpenPlayerSaveEvent extends PlayerSaveEvent {
private final ISpecialInventory inventory;
/**
* Construct a new {@code OpenPlayerSaveEvent}.
*
* <p>The constructor is not considered part of the API, and may be subject to change.</p>
*
* @param player the player to be saved
* @param inventory the {@link ISpecialInventory} being closed
*/
@RestrictedApi(
explanation = "Constructor is not considered part of the API and may be subject to change.",
link = "",
allowedOnPath = ".*/com/lishid/openinv/event/OpenEvents.java")
@ApiStatus.Internal
OpenPlayerSaveEvent(@NotNull Player player, @NotNull ISpecialInventory inventory) {
super(player);
this.inventory = inventory;
}
/**
* Get the {@link ISpecialInventory} that triggered the save by being closed.
*
* @return the special inventory
*/
public @NotNull ISpecialInventory getInventory() {
return inventory;
}
}
@@ -1,67 +0,0 @@
package com.lishid.openinv.event;
import com.google.errorprone.annotations.RestrictedApi;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Event fired before a {@link Player} loaded via OpenInv is saved.
*/
public class PlayerSaveEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private boolean cancelled = false;
/**
* Construct a new {@code PlayerSaveEvent}.
*
* <p>The constructor is not considered part of the API, and may be subject to change.</p>
*
* @param player the player to be saved
*/
@RestrictedApi(
explanation = "Constructor is not considered part of the API and may be subject to change.",
link = "",
allowedOnPath = ".*/com/lishid/openinv/event/(OpenPlayerSaveEvent|OpenEvents).java")
@ApiStatus.Internal
PlayerSaveEvent(@NotNull Player player) {
super(player);
}
/**
* Get whether the event is cancelled.
*
* @return true if the event is cancelled
*/
@Override
public boolean isCancelled() {
return cancelled;
}
/**
* Set whether the event is cancelled.
*
* @param cancel whether the event is cancelled
*/
@Override
public void setCancelled(boolean cancel) {
this.cancelled = cancel;
}
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLERS;
}
public static HandlerList getHandlerList() {
return HANDLERS;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,6 +16,7 @@
package com.lishid.openinv.internal;
import org.bukkit.Material;
import org.bukkit.block.Barrel;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
@@ -23,38 +24,48 @@ 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;
import org.bukkit.util.BoundingBox;
import org.jetbrains.annotations.NotNull;
public interface IAnySilentContainer {
/**
* Forcibly open the container at the given coordinates for the Player. This will open blocked containers! Be sure
* to check {@link #isAnyContainerNeeded(Block)} first if that is not desirable.
* Opens the container at the given coordinates for the Player. If you do not want blocked
* containers to open, be sure to check {@link #isAnyContainerNeeded(Player, Block)}
* first.
*
* @param player the {@link Player} opening the container
* @param silent whether the container's noise is to be silenced
* @param block the {@link Block} of the container
* @param player the Player opening the container
* @param silent whether the container's noise is to be silenced
* @param block the Block
* @return true if the container can be opened
*/
boolean activateContainer(@NotNull Player player, boolean silent, @NotNull Block block);
/**
* Perform operations required to close the current container silently.
* Closes the Player's currently open container silently, if necessary.
*
* @param player the {@link Player} closing a container
* @param player the Player closing a container
*/
void deactivateContainer(@NotNull Player player);
/**
* Check if the container at the given coordinates is blocked.
* @deprecated use {@link #isAnyContainerNeeded(Block)}
* @param player the player opening the container
* @param block the block
* @return true if the container is blocked
*/
@Deprecated
default boolean isAnyContainerNeeded(@NotNull Player player, @NotNull Block block) {
return isAnyContainerNeeded(block);
}
/**
* Checks if the container at the given coordinates is blocked.
*
* @param block the {@link Block} of the container
* @param block the Block
* @return true if the container is blocked
*/
default boolean isAnyContainerNeeded(@NotNull Block block) {
@@ -70,9 +81,11 @@ public interface IAnySilentContainer {
return block.getRelative(0, 1, 0).getType().isOccluding();
}
// Shulker boxes require half a block clear in the direction they open.
// Shulker boxes require 1/2 a block clear in the direction they open.
if (blockState instanceof ShulkerBox) {
return isShulkerBlocked(block);
if (isShulkerBlocked((ShulkerBox) blockState)) {
return true;
}
}
if (!(blockState instanceof org.bukkit.block.Chest)) {
@@ -84,28 +97,25 @@ public interface IAnySilentContainer {
}
BlockData blockData = block.getBlockData();
if (!(blockData instanceof Chest chest) || chest.getType() == Chest.Type.SINGLE) {
if (!(blockData instanceof Chest) || ((Chest) blockData).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);
Chest chest = (Chest) blockData;
int ordinal = (chest.getFacing().ordinal() + 4 + (chest.getType() == Chest.Type.RIGHT ? -1 : 1)) % 4;
BlockFace relativeFace = BlockFace.values()[ordinal];
org.bukkit.block.Block relative = block.getRelative(relativeFace);
if (relative.getType() != block.getType()) {
return false;
}
BlockData relativeData = relative.getBlockData();
if (!(relativeData instanceof Chest relativeChest)) {
if (!(relativeData instanceof Chest)) {
return false;
}
Chest relativeChest = (Chest) relativeData;
if (relativeChest.getFacing() != chest.getFacing()
|| relativeChest.getType() != (chest.getType() == Chest.Type.RIGHT ? Chest.Type.LEFT : Chest.Type.RIGHT)) {
return false;
@@ -114,66 +124,34 @@ public interface IAnySilentContainer {
return isChestBlocked(relative);
}
/**
* Check if a shulker box block cannot be opened under ordinary circumstances.
*
* @param shulkerBox the shulker box block
* @return whether the container is blocked
*/
default boolean isShulkerBlocked(@NotNull Block shulkerBox) {
Directional directional = (Directional) shulkerBox.getBlockData();
BlockFace facing = directional.getFacing();
BoundingBox box = new BoundingBox(0, 0, 0, 1, 1, 1);
box.expand(facing, 0.5);
box.shift(facing.getOppositeFace().getDirection());
return shulkerBox.getRelative(facing).getCollisionShape().overlaps(box);
}
boolean isShulkerBlocked(ShulkerBox block);
/**
* Check if a chest cannot be opened under ordinary circumstances.
* Determine whether or not a chest is blocked.
*
* @param chest the chest block
* @return whether the container is blocked
* @return true if the chest block cannot be opened under ordinary circumstances
*/
default boolean isChestBlocked(@NotNull Block chest) {
default boolean isChestBlocked(Block chest) {
org.bukkit.block.Block relative = chest.getRelative(0, 1, 0);
return relative.getType().isOccluding()
|| !chest.getWorld().getNearbyEntities(BoundingBox.of(relative), Cat.class::isInstance).isEmpty();
|| chest.getWorld().getNearbyEntities(BoundingBox.of(relative), entity -> entity instanceof Cat).size() > 0;
}
/**
* Check if the given {@link Block} is a container which can be unblocked or silenced.
* Checks if the given block is a container which can be unblocked or silenced.
*
* @param block the potential container
* @return true if the type is a supported container
* @param block the BlockState
* @return true if the Block is a supported container
*/
default boolean isAnySilentContainer(@NotNull Block block) {
return isAnySilentContainer(block.getState());
}
/**
* Check if the given {@link BlockState} is a container which can be unblocked or silenced.
*
* @param blockState the potential container
* @return true if the type is a supported container
*/
default boolean isAnySilentContainer(@NotNull BlockState blockState) {
return (blockState instanceof InventoryHolder holder && isAnySilentContainer(holder))
|| blockState instanceof EnderChest;
}
/**
* Check if the given {@link InventoryHolder} is a container which can be unblocked or silenced.
*
* @param holder the potential container
* @return true if the type is a supported container
*/
default boolean isAnySilentContainer(@NotNull InventoryHolder holder) {
return holder instanceof org.bukkit.block.EnderChest
|| holder instanceof org.bukkit.block.Chest
|| holder instanceof org.bukkit.block.DoubleChest
|| holder instanceof org.bukkit.block.ShulkerBox
|| holder instanceof org.bukkit.block.Barrel;
if (block.getType() == Material.ENDER_CHEST) {
return true;
}
BlockState state = block.getState();
return state instanceof org.bukkit.block.Chest
|| state instanceof org.bukkit.block.ShulkerBox
|| state instanceof org.bukkit.block.Barrel;
}
}
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@Deprecated
public interface IInventoryAccess {
/**
* Gets an ISpecialEnderChest from an Inventory or null if the Inventory is not backed by an
* ISpecialEnderChest.
*
* @param inventory the Inventory
* @return the ISpecialEnderChest or null
*/
@Deprecated
@Nullable ISpecialEnderChest getSpecialEnderChest(@NotNull Inventory inventory);
/**
* Gets an ISpecialPlayerInventory from an Inventory or null if the Inventory is not backed by
* an ISpecialPlayerInventory.
*
* @param inventory the Inventory
* @return the ISpecialPlayerInventory or null
*/
@Deprecated
@Nullable ISpecialPlayerInventory getSpecialPlayerInventory(@NotNull Inventory inventory);
/**
* Check if an Inventory is an ISpecialEnderChest implementation.
*
* @param inventory the Inventory
* @return true if the Inventory is backed by an ISpecialEnderChest
*/
@Deprecated
boolean isSpecialEnderChest(@NotNull Inventory inventory);
/**
* Check if an Inventory is an ISpecialPlayerInventory implementation.
*
* @param inventory the Inventory
* @return true if the Inventory is backed by an ISpecialPlayerInventory
*/
@Deprecated
boolean isSpecialPlayerInventory(@NotNull Inventory inventory);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,9 +16,6 @@
package com.lishid.openinv.internal;
/**
* An {@link ISpecialInventory} representing an ender chest.
*/
public interface ISpecialEnderChest extends ISpecialInventory {
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -21,43 +21,38 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
/**
* Interface defining behavior for special inventories backed by other inventories' content listings.
*/
public interface ISpecialInventory {
/**
* Get the {@link Inventory} associated with this {@code ISpecialInventory}.
* Gets the Inventory associated with this ISpecialInventory.
*
* @return the Bukkit inventory
* @return the Inventory
*/
@NotNull Inventory getBukkitInventory();
/**
* Set the owning {@link Player} instance to a newly-joined user.
* Sets the Player associated with this ISpecialInventory online.
*
* @param player the user coming online
* @param player the Player coming online
*/
void setPlayerOnline(@NotNull Player player);
/**
* Mark the owner of the inventory offline.
* Sets the Player associated with this ISpecialInventory offline.
*/
void setPlayerOffline();
/**
* Get whether the inventory is being viewed by any users.
* Gets whether or not this ISpecialInventory is in use.
*
* @return true if the inventory is being viewed
* @return true if the ISpecialInventory is in use
*/
default boolean isInUse() {
return !getBukkitInventory().getViewers().isEmpty();
}
boolean isInUse();
/**
* Get the {@link Player} who owns the inventory.
* Gets the Player associated with this ISpecialInventory.
*
* @return the {@link HumanEntity} who owns the inventory
* @return the HumanEntity
*/
@NotNull HumanEntity getPlayer();
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,35 +16,6 @@
package com.lishid.openinv.internal;
import java.util.List;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.Inventory;
/**
* An {@link ISpecialInventory} representing a player inventory.
*/
public interface ISpecialPlayerInventory extends ISpecialInventory {
/*
* Player inventory usage varies from all other inventories - as the inventory is technically open at all times,
* if the player is online or has been online while the inventory is open, they are in the viewer list.
*/
@Override
default boolean isInUse() {
Inventory inventory = getBukkitInventory();
List<HumanEntity> viewers = inventory.getViewers();
if (viewers.size() != 1) {
return !viewers.isEmpty();
}
HumanEntity viewer = viewers.get(0);
if (!viewer.getUniqueId().equals(getPlayer().getUniqueId())) {
return true;
}
return viewer.getOpenInventory().getTopInventory().equals(inventory);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,89 +16,102 @@
package com.lishid.openinv.util;
import com.google.errorprone.annotations.RestrictedApi;
import com.lishid.openinv.internal.IInventoryAccess;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import java.lang.reflect.Method;
import org.bukkit.Bukkit;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.BiFunction;
public class InventoryAccess implements IInventoryAccess {
public final class InventoryAccess {
private static Class<?> craftInventory = null;
private static Method getInventory = null;
private static @Nullable BiFunction<Inventory, Class<? extends ISpecialInventory>, ISpecialInventory> provider;
public static boolean isUsable() {
return provider != null;
static {
String packageName = Bukkit.getServer().getClass().getPackage().getName();
try {
craftInventory = Class.forName(packageName + ".inventory.CraftInventory");
} catch (ClassNotFoundException ignored) {}
try {
getInventory = craftInventory.getDeclaredMethod("getInventory");
} catch (NoSuchMethodException ignored) {}
}
/**
* Check if an {@link Inventory} is an {@link ISpecialPlayerInventory} implementation.
*
* @param inventory the Bukkit inventory
* @return true if backed by the correct implementation
* @deprecated use {@link #isUsable()}
*/
@Deprecated
public static boolean isUseable() {
return isUsable();
}
public static boolean isUsable() {
return craftInventory != null && getInventory != null;
}
public static boolean isPlayerInventory(@NotNull Inventory inventory) {
return getPlayerInventory(inventory) != null;
}
/**
* Get the {@link ISpecialPlayerInventory} backing an {@link Inventory}. Returns {@code null} if the inventory is
* not backed by the correct class.
*
* @param inventory the Bukkit inventory
* @return the backing implementation if available
*/
public static @Nullable ISpecialPlayerInventory getPlayerInventory(@NotNull Inventory inventory) {
return provider == null ? null : (ISpecialPlayerInventory) provider.apply(inventory, ISpecialPlayerInventory.class);
return getSpecialInventory(ISpecialPlayerInventory.class, inventory);
}
/**
* Check if an {@link Inventory} is an {@link ISpecialEnderChest} implementation.
*
* @param inventory the Bukkit inventory
* @return true if backed by the correct implementation
*/
public static boolean isEnderChest(@NotNull Inventory inventory) {
return getEnderChest(inventory) != null;
}
/**
* Get the {@link ISpecialEnderChest} backing an {@link Inventory}. Returns {@code null} if the inventory is
* not backed by the correct class.
*
* @param inventory the Bukkit inventory
* @return the backing implementation if available
*/
public static @Nullable ISpecialEnderChest getEnderChest(@NotNull Inventory inventory) {
return provider == null ? null : (ISpecialEnderChest) provider.apply(inventory, ISpecialEnderChest.class);
return getSpecialInventory(ISpecialEnderChest.class, inventory);
}
/**
* Get a {@link ISpecialInventory} backing an {@link Inventory}. Returns {@code null} if the inventory is not backed
* by the correct class.
*
* @param inventory the Bukkit inventory
* @return the backing implementation if available
*/
public static @Nullable ISpecialInventory getInventory(@NotNull Inventory inventory) {
return provider == null ? null : provider.apply(inventory, ISpecialInventory.class);
private static <T extends ISpecialInventory> @Nullable T getSpecialInventory(@NotNull Class<T> expected, @NotNull Inventory inventory) {
Object inv;
if (craftInventory != null && getInventory != null && craftInventory.isAssignableFrom(inventory.getClass())) {
try {
inv = getInventory.invoke(inventory);
if (expected.isInstance(inv)) {
return expected.cast(inv);
}
} catch (ReflectiveOperationException ignored) {}
}
// Use reflection to find the IInventory
inv = ReflectionHelper.grabObjectByType(inventory, expected);
if (expected.isInstance(inv)) {
return expected.cast(inv);
}
return null;
}
@RestrictedApi(
explanation = "Not part of the API.",
link = "",
allowedOnPath = ".*/com/lishid/openinv/util/InternalAccessor.java")
@ApiStatus.Internal
static void setProvider(@Nullable BiFunction<Inventory, Class<? extends ISpecialInventory>, ISpecialInventory> provider) {
InventoryAccess.provider = provider;
@Deprecated
@Override
public @Nullable ISpecialEnderChest getSpecialEnderChest(@NotNull Inventory inventory) {
return getEnderChest(inventory);
}
private InventoryAccess() {
throw new IllegalStateException("Cannot create instance of utility class.");
@Deprecated
@Override
public @Nullable ISpecialPlayerInventory getSpecialPlayerInventory(@NotNull Inventory inventory) {
return getPlayerInventory(inventory);
}
@Deprecated
@Override
public boolean isSpecialEnderChest(@NotNull Inventory inventory) {
return isEnderChest(inventory);
}
@Deprecated
@Override
public boolean isSpecialPlayerInventory(@NotNull Inventory inventory) {
return isPlayerInventory(inventory);
}
}
@@ -16,15 +16,16 @@
package com.lishid.openinv.util;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import org.jetbrains.annotations.Nullable;
/**
* A utility for making reflection easier.
*/
public final class ReflectionHelper {
private ReflectionHelper() {}
/**
* Grab an {@link Object} stored in a {@link Field} of another {@code Object}.
*
@@ -58,8 +59,8 @@ public final class ReflectionHelper {
*/
public static @Nullable Field grabFieldByType(Class<?> holderType, Class<?> fieldType) {
for (Field field : holderType.getDeclaredFields()) {
field.setAccessible(true);
if (fieldType.isAssignableFrom(field.getType())) {
field.setAccessible(true);
return field;
}
}
@@ -71,8 +72,4 @@ public final class ReflectionHelper {
return null;
}
private ReflectionHelper() {
throw new IllegalStateException("Cannot create instance of utility class.");
}
}
+58
View File
@@ -0,0 +1,58 @@
<!--
~ Copyright (C) 2011-2021 lishid. All rights reserved.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<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>
<groupId>com.lishid</groupId>
<artifactId>openinvparent</artifactId>
<version>4.1.9-SNAPSHOT</version>
</parent>
<artifactId>openinvassembly</artifactId>
<name>OpenInvAssembly</name>
<packaging>pom</packaging>
<build>
<directory>../target</directory>
<finalName>OpenInv</finalName>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>reactor-uberjar</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptors>
<descriptor>src/assembly/reactor-uberjar.xml</descriptor>
</descriptors>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
+54
View File
@@ -0,0 +1,54 @@
<!--
~ Copyright (C) 2011-2021 lishid. All rights reserved.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, version 3.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.1.0 http://maven.apache.org/xsd/assembly-2.1.0.xsd">
<id>reactor-uberjar</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<moduleSets>
<moduleSet>
<useAllReactorProjects>true</useAllReactorProjects>
<binaries>
<outputDirectory>/</outputDirectory>
<unpack>true</unpack>
<unpackOptions>
<excludes>
<!--
- Exclude existing meta - assembly will write its own manifest, and the rest is maven
- details that end users don't need. Ignoring it saves around 7KB after compression
- with a single internal module present.
-->
<exclude>META-INF/**</exclude>
</excludes>
</unpackOptions>
<!-- Exclude dependencies - shade plugin will handle their inclusion as necessary in individual jars. -->
<includeDependencies>false</includeDependencies>
</binaries>
</moduleSet>
</moduleSets>
</assembly>
-30
View File
@@ -1,30 +0,0 @@
<?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>
<groupId>com.lishid</groupId>
<artifactId>openinvparent</artifactId>
<version>5.0.0</version>
</parent>
<artifactId>openinvcommon</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>
</dependency>
</dependencies>
</project>
@@ -1,32 +0,0 @@
package com.lishid.openinv.event;
import com.lishid.openinv.internal.ISpecialInventory;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
/**
* Construct and call events.
*/
public final class OpenEvents {
public static boolean saveCancelled(@NotNull Player player) {
return call(new PlayerSaveEvent(player));
}
public static boolean saveCancelled(@NotNull Player player, @NotNull ISpecialInventory inventory) {
return call(new OpenPlayerSaveEvent(player, inventory));
}
private static <T extends Event & Cancellable> boolean call(T event) {
Bukkit.getPluginManager().callEvent(event);
return event.isCancelled();
}
private OpenEvents() {
throw new IllegalStateException("Cannot create instance of utility class.");
}
}
@@ -1,24 +0,0 @@
package com.lishid.openinv.internal;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface Accessor {
@NotNull
PlayerManager getPlayerManager();
@NotNull IAnySilentContainer getAnySilentContainer();
@NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player);
@NotNull ISpecialEnderChest createEnderChest(@NotNull Player player);
<T extends ISpecialInventory> @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class<T> clazz);
void reload(@NotNull ConfigurationSection config);
}
@@ -1,48 +0,0 @@
package com.lishid.openinv.internal;
import com.lishid.openinv.util.lang.LanguageManager;
import com.lishid.openinv.util.lang.Replacement;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
public enum InventoryViewTitle {
PLAYER_INVENTORY("container.player", "'s Inventory"),
ENDER_CHEST("container.enderchest", "'s Ender Chest");
private final String localizationKey;
private final String defaultSuffix;
InventoryViewTitle(String localizationKey, String defaultSuffix) {
this.localizationKey = localizationKey;
this.defaultSuffix = defaultSuffix;
}
public @NotNull String getTitle(
@NotNull LanguageManager lang,
@NotNull Player viewer,
@NotNull ISpecialInventory inventory) {
HumanEntity owner = inventory.getPlayer();
String localTitle = lang.getLocalizedMessage(
viewer,
localizationKey,
new Replacement("%player%", owner.getName()));
return Objects.requireNonNullElseGet(localTitle, () -> owner.getName() + defaultSuffix);
}
public static @Nullable InventoryViewTitle of(@NotNull ISpecialInventory inventory) {
if (inventory instanceof ISpecialPlayerInventory) {
return PLAYER_INVENTORY;
} else if (inventory instanceof ISpecialEnderChest) {
return ENDER_CHEST;
} else {
return null;
}
}
}
@@ -1,9 +0,0 @@
package com.lishid.openinv.internal;
import org.bukkit.configuration.ConfigurationSection;
public interface PlaceholderParser {
void load(ConfigurationSection section) throws Exception;
}
@@ -1,60 +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 org.bukkit.permissions.Permissible;
import org.jetbrains.annotations.NotNull;
public enum Permissions {
INVENTORY_OPEN_SELF("inventory.open.self"),
INVENTORY_OPEN_OTHER("inventory.open.other"),
INVENTORY_EDIT_SELF("inventory.edit.self"),
INVENTORY_EDIT_OTHER("inventory.edit.other"),
INVENTORY_SLOT_HEAD_ANY("inventory.slot.head.any"),
INVENTORY_SLOT_CHEST_ANY("inventory.slot.chest.any"),
INVENTORY_SLOT_LEGS_ANY("inventory.slot.legs.any"),
INVENTORY_SLOT_FEET_ANY("inventory.slot.feet.any"),
INVENTORY_SLOT_DROP("inventory.slot.drop"),
ENDERCHEST_OPEN_SELF("enderchest.open.self"),
ENDERCHEST_OPEN_OTHER("enderchest.open.other"),
ENDERCHEST_EDIT_SELF("enderchest.edit.self"),
ENDERCHEST_EDIT_OTHER("enderchest.edit.other"),
ACCESS_CROSSWORLD("access.crossworld"),
ACCESS_OFFLINE("access.offline"),
ACCESS_ONLINE("access.online"),
SPECTATE_CLICK("spectate.click"),
CONTAINER_ANY("container.any"),
CONTAINER_SILENT("container.silent"),
SEARCH_INVENTORY("search.inventory"),
SEARCH_CONTAINER("search.container");
private final String permission;
Permissions(String permission) {
this.permission = "openinv." + permission;
}
public boolean hasPermission(@NotNull Permissible permissible) {
return permissible.hasPermission(permission);
}
}
@@ -1,319 +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.lang;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.ChatColor;
import org.bukkit.command.CommandSender;
import org.bukkit.configuration.Configuration;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import java.util.logging.Level;
/**
* A simple language manager supporting both custom and bundled languages.
*
* @author Jikoo
*/
public class LanguageManager {
private final Plugin plugin;
private final File folder;
private final String defaultLocale;
private final Map<String, YamlConfiguration> locales;
public LanguageManager(@NotNull Plugin plugin, @NotNull String defaultLocale) {
this.plugin = plugin;
this.defaultLocale = defaultLocale;
this.locales = new HashMap<>();
this.folder = new File(plugin.getDataFolder(), "locale");
if (!folder.exists() && !folder.mkdirs()) {
plugin.getLogger().warning(() -> "Unable to create " + folder.getPath() + "! Languages may not be editable.");
}
reload();
}
public void reload() {
this.locales.clear();
getOrLoadLocale(defaultLocale);
}
private @NotNull YamlConfiguration getOrLoadLocale(@NotNull String locale) {
YamlConfiguration loaded = locales.get(locale);
if (loaded != null) {
return loaded;
}
LangLocation lang = bestMatch(locale, null);
// If a parent was a better match, check if it is already loaded.
if (!locale.equals(lang.locale)) {
loaded = locales.get(lang.locale);
if (loaded != null) {
locales.put(locale, loaded);
return loaded;
}
}
// Load locale config from disk and bundled locale defaults.
YamlConfiguration localeConfig = loadLocale(lang);
// If the locale is not the default locale, also handle any missing translations from the default locale.
if (!locale.equals(defaultLocale)) {
addTranslationFallthrough(lang, localeConfig);
if (plugin.getConfig().getBoolean("settings.secret.warn-about-guess-section", true)
&& localeConfig.isConfigurationSection("guess")) {
// Warn that guess section exists. This should run once per language per server restart
// when accessed by a user to hint to server owners that they can make UX improvements.
plugin.getLogger().info(() -> "[LanguageManager] Missing translations from " + lang.locale + ".yml! Check the guess section!");
}
}
locales.put(locale, localeConfig);
locales.put(lang.locale, localeConfig);
return localeConfig;
}
private @NotNull LangLocation bestMatch(@NotNull String locale, @Nullable LangLocation initial) {
File file = new File(folder, locale + ".yml");
InputStream bundled = plugin.getResource("locale/" + locale + ".yml");
if (file.exists() || bundled != null) {
return new LangLocation(locale, file, bundled);
}
if (initial == null) {
initial = new LangLocation(locale, file, null);
}
int lastSeparator = locale.lastIndexOf('_');
// Must be at least some content before separator.
if (lastSeparator < 1) {
return initial;
}
return bestMatch(locale.substring(0, lastSeparator), initial);
}
private @NotNull YamlConfiguration loadLocale(@NotNull LangLocation lang) {
YamlConfiguration localeConfigDefaults;
if (lang.bundled == null) {
localeConfigDefaults = new YamlConfiguration();
} else {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(lang.bundled, StandardCharsets.UTF_8))) {
localeConfigDefaults = YamlConfiguration.loadConfiguration(reader);
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to load resource " + lang.locale + ".yml");
localeConfigDefaults = new YamlConfiguration();
}
}
if (!lang.file.exists()) {
// If the file does not exist on disk, save bundled defaults.
try {
localeConfigDefaults.save(lang.file);
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + lang.locale + ".yml");
}
// Return loaded bundled locale.
return localeConfigDefaults;
}
// If the file does exist on disk, load it.
YamlConfiguration localeConfig = YamlConfiguration.loadConfiguration(lang.file);
// Check for missing translations from the bundled file.
List<String> newKeys = getMissingKeys(localeConfigDefaults, localeConfig::isSet);
if (newKeys.isEmpty()) {
return localeConfig;
}
// Get guess section for missing keys.
ConfigurationSection guess = localeConfig.getConfigurationSection("guess");
for (String newKey : newKeys) {
// Set all missing keys to defaults.
localeConfig.set(newKey, localeConfigDefaults.get(newKey));
// Delete relevant guess keys in case this is a new translation.
if (guess != null) {
guess.set(newKey, null);
}
}
// If guess section is empty, delete it.
if (guess != null && guess.getKeys(false).isEmpty()) {
localeConfig.set("guess", null);
}
plugin.getLogger().info(() -> "[LanguageManager] Added new translation keys to " + lang.locale + ".yml: " + String.join(", ", newKeys));
// Write new keys to disk.
try {
localeConfig.save(lang.file);
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + lang.locale + ".yml");
}
return localeConfig;
}
private void addTranslationFallthrough(@NotNull LangLocation location, @NotNull YamlConfiguration localeConfig) {
YamlConfiguration defaultLocaleConfig = locales.get(defaultLocale);
// Get missing keys. Keys that already have a guess value are not new and don't need to trigger another write.
List<String> missingKeys = getMissingKeys(
defaultLocaleConfig,
key -> localeConfig.isSet(key) || localeConfig.isSet("guess." + key));
if (!missingKeys.isEmpty()) {
// Set up guess section for missing keys.
for (String key : missingKeys) {
localeConfig.set("guess." + key, defaultLocaleConfig.get(key));
}
// Write modified guess section to disk.
try {
localeConfig.save(location.file);
} catch (IOException e) {
plugin.getLogger().log(Level.WARNING, e, () -> "[LanguageManager] Unable to save resource " + location.locale + ".yml");
}
}
// Fall through to default locale.
localeConfig.setDefaults(defaultLocaleConfig);
}
private @NotNull List<String> getMissingKeys(
@NotNull Configuration configurationDefault,
@NotNull Predicate<String> nodeSetPredicate) {
List<String> missingKeys = new ArrayList<>();
for (String key : configurationDefault.getKeys(true)) {
if (!configurationDefault.isConfigurationSection(key) && !nodeSetPredicate.test(key)) {
// Missing keys are non-section keys that fail the predicate.
missingKeys.add(key);
}
}
return missingKeys;
}
public @Nullable String getValue(@NotNull String key, @Nullable String locale) {
String value = getOrLoadLocale(locale == null ? defaultLocale : locale.toLowerCase(Locale.ENGLISH)).getString(key);
if (value == null || value.isBlank()) {
return null;
}
value = ChatColor.translateAlternateColorCodes('&', value);
return value;
}
public @Nullable String getValue(@NotNull String key, @Nullable String locale, Replacement @NotNull ... replacements) {
String value = getValue(key, locale);
if (value == null) {
return null;
}
for (Replacement replacement : replacements) {
value = value.replace(replacement.placeholder(), replacement.value());
}
return value;
}
public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key) {
return getValue(key, getLocale(sender));
}
public @Nullable String getLocalizedMessage(
@NotNull CommandSender sender,
@NotNull String key,
Replacement @NotNull ... replacements) {
return getValue(key, getLocale(sender), replacements);
}
private @NotNull String getLocale(@NotNull CommandSender sender) {
if (sender instanceof Player) {
return ((Player) sender).getLocale();
} else {
return plugin.getConfig().getString("settings.locale", "en_us");
}
}
public void sendMessage(@NotNull CommandSender sender, @NotNull String key) {
String message = getLocalizedMessage(sender, key);
if (message != null && !message.isEmpty()) {
sender.sendMessage(message);
}
}
public void sendMessage(@NotNull CommandSender sender, @NotNull String key, Replacement @NotNull... replacements) {
String message = getLocalizedMessage(sender, key, replacements);
if (message != null && !message.isEmpty()) {
sender.sendMessage(message);
}
}
public void sendSystemMessage(@NotNull Player player, @NotNull String key) {
String message = getLocalizedMessage(player, key);
if (message == null) {
return;
}
int newline = message.indexOf('\n');
if (newline != -1) {
// No newlines in action bar chat.
message = message.substring(0, newline);
}
if (message.isEmpty()) {
return;
}
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacy(message));
}
private record LangLocation(@NotNull String locale, @NotNull File file, @Nullable InputStream bundled) {}
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2011-2023 lishid. All rights reserved.
~ Copyright (C) 2011-2021 lishid. All rights reserved.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
@@ -23,29 +23,23 @@
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<relativePath>../../pom.xml</relativePath>
<version>5.0.0</version>
<version>4.1.9-SNAPSHOT</version>
</parent>
<artifactId>openinvadapter1_20_R4</artifactId>
<name>OpenInvAdapter1_20_R4</name>
<artifactId>openinvadapter1_17_R1</artifactId>
<name>OpenInvAdapter1_17_R1</name>
<properties>
<maven.compiler.system>21</maven.compiler.system>
<maven.compiler.release>21</maven.compiler.release>
<spigot.version>1.20.6-R0.1-SNAPSHOT</spigot.version>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>${spigot.version}</version>
</dependency>
<dependency>
<artifactId>spigot</artifactId>
<groupId>org.spigotmc</groupId>
<scope>provided</scope>
<version>${spigot.version}</version>
<version>1.17.1-R0.1-SNAPSHOT</version>
<classifier>remapped-mojang</classifier>
</dependency>
<dependency>
@@ -53,7 +47,7 @@
<groupId>com.lishid</groupId>
</dependency>
<dependency>
<artifactId>openinvcommon</artifactId>
<artifactId>openinvplugincore</artifactId>
<groupId>com.lishid</groupId>
</dependency>
<dependency>
@@ -64,12 +58,46 @@
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>net.md-5</groupId>
<artifactId>specialsource-maven-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>remap</goal>
</goals>
<id>remap-obf</id>
<configuration>
<srgIn>org.spigotmc:minecraft-server:1.17.1-R0.1-SNAPSHOT:txt:maps-mojang</srgIn>
<reverse>true</reverse>
<remappedDependencies>org.spigotmc:spigot:1.17.1-R0.1-SNAPSHOT:jar:remapped-mojang</remappedDependencies>
<remappedArtifactAttached>true</remappedArtifactAttached>
<remappedClassifierName>remapped-obf</remappedClassifierName>
</configuration>
</execution>
<execution>
<phase>package</phase>
<goals>
<goal>remap</goal>
</goals>
<id>remap-spigot</id>
<configuration>
<inputFile>${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf.jar</inputFile>
<srgIn>org.spigotmc:minecraft-server:1.17.1-R0.1-SNAPSHOT:csrg:maps-spigot</srgIn>
<remappedDependencies>org.spigotmc:spigot:1.17.1-R0.1-SNAPSHOT:jar:remapped-obf</remappedDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,18 +14,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_21_R1.container;
package com.lishid.openinv.internal.v1_17_R1;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.v1_21_R1.player.PlayerManager;
import com.lishid.openinv.util.ReflectionHelper;
import com.lishid.openinv.util.lang.LanguageManager;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.world.CompoundContainer;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.monster.Shulker;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.PlayerEnderChestContainer;
@@ -33,36 +39,39 @@ import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.BarrelBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.DoubleBlockCombiner;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.TrappedChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.GameMode;
import net.minecraft.world.phys.AABB;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.block.ShulkerBox;
import org.bukkit.craftbukkit.v1_17_R1.CraftWorld;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.logging.Logger;
public class AnySilentContainer implements IAnySilentContainer {
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
private @Nullable Field serverPlayerGameModeGameType;
public AnySilentContainer(@NotNull Logger logger, @NotNull LanguageManager lang) {
this.logger = logger;
this.lang = lang;
public AnySilentContainer() {
try {
try {
// IDE warns about field not existing, but SpecialSource does not remap strings used in reflection.
// The warning is not suppressed as a reminder that it must manually be checked on updates.
this.serverPlayerGameModeGameType = ServerPlayerGameMode.class.getDeclaredField("b");
this.serverPlayerGameModeGameType.setAccessible(true);
} catch (NoSuchFieldException e) {
Logger logger = OpenInv.getPlugin(OpenInv.class).getLogger();
logger.warning("ServerPlayerGameMode#gameModeForPlayer's obfuscated name has changed!");
logger.warning("Please report this at https://github.com/Jikoo/OpenInv/issues");
logger.warning("Attempting to fall through using reflection. Please verify that SilentContainer does not fail.");
@@ -70,11 +79,43 @@ public class AnySilentContainer implements IAnySilentContainer {
this.serverPlayerGameModeGameType = ReflectionHelper.grabFieldByType(ServerPlayerGameMode.class, GameType.class);
}
} catch (SecurityException e) {
Logger logger = OpenInv.getPlugin(OpenInv.class).getLogger();
logger.warning("Unable to directly write player game mode! SilentContainer will fail.");
logger.log(java.util.logging.Level.WARNING, "Error obtaining GameType field", e);
logger.log(Level.WARNING, "Error obtaining GameType field", e);
}
}
@Override
public boolean isShulkerBlocked(ShulkerBox box) {
org.bukkit.World bukkitWorld = box.getWorld();
if (!(bukkitWorld instanceof CraftWorld)) {
bukkitWorld = Bukkit.getWorld(bukkitWorld.getUID());
}
if (!(bukkitWorld instanceof CraftWorld craftWorld)) {
Exception exception = new IllegalStateException("AnySilentContainer access attempted on an unknown world!");
OpenInv.getPlugin(OpenInv.class).getLogger().log(Level.WARNING, exception.getMessage(), exception);
return false;
}
final ServerLevel world = craftWorld.getHandle();
final BlockPos blockPosition = new BlockPos(box.getX(), box.getY(), box.getZ());
final BlockEntity tile = world.getBlockEntity(blockPosition);
if (!(tile instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity)
|| shulkerBoxBlockEntity.getAnimationStatus() != ShulkerBoxBlockEntity.AnimationStatus.CLOSED) {
return false;
}
BlockState blockState = world.getBlockState(blockPosition);
// See net.minecraft.world.level.block.ShulkerBoxBlock#canOpen
AABB boundingBox = Shulker.getProgressDeltaAabb(blockState.getValue(ShulkerBoxBlock.FACING), 0.0F, 0.5F)
.move(blockPosition)
.deflate(1.0E-6D);
return !world.noCollision(boundingBox);
}
@Override
public boolean activateContainer(
@NotNull final Player bukkitPlayer,
@@ -88,9 +129,9 @@ public class AnySilentContainer implements IAnySilentContainer {
return true;
}
ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
ServerPlayer player = PlayerDataManager.getHandle(bukkitPlayer);
final net.minecraft.world.level.Level level = player.level();
final ServerLevel level = player.getLevel();
final BlockPos blockPos = new BlockPos(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ());
final BlockEntity blockEntity = level.getBlockEntity(blockPos);
@@ -99,14 +140,14 @@ public class AnySilentContainer implements IAnySilentContainer {
}
if (blockEntity instanceof EnderChestBlockEntity enderChestTile) {
// Anychest ender chest. See net.minecraft.world.level.block.EnderChestBlock
// Anychest ender chest. See net.minecraft.world.level.block.BlockEnderChest
PlayerEnderChestContainer enderChest = player.getEnderChestInventory();
enderChest.setActiveChest(enderChestTile);
player.openMenu(new SimpleMenuProvider((containerCounter, playerInventory, ignored) -> {
MenuType<?> containers = OpenContainerMenu.getContainers(enderChest.getContainerSize());
MenuType<?> containers = PlayerDataManager.getContainers(enderChest.getContainerSize());
int rows = enderChest.getContainerSize() / 9;
return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows);
}, Component.translatable("container.enderchest")));
}, new TextComponent("container.enderchest")));
bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
return true;
}
@@ -120,14 +161,34 @@ public class AnySilentContainer implements IAnySilentContainer {
if (block instanceof ChestBlock chestBlock) {
// boolean flag: do not check if chest is blocked
menuProvider = chestBlock.getMenuProvider(blockState, level, blockPos, true);
// boolean flag: check if chest is blocked
Optional<MenuProvider> menuOptional = chestBlock.combine(blockState, level, blockPos, false).apply(
// Combiner is a copy of private ChestBlock.MENU_PROVIDER_COMBINER
new DoubleBlockCombiner.Combiner<ChestBlockEntity, Optional<MenuProvider>>() {
@Override
public Optional<MenuProvider> acceptDouble(ChestBlockEntity localChest1, ChestBlockEntity localChest2) {
CompoundContainer doubleChest = new CompoundContainer(localChest1, localChest2);
return Optional.of(new ChestBlock.DoubleInventory(localChest1, localChest2, doubleChest));
}
if (menuProvider == null) {
lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
@Override
public Optional<MenuProvider> acceptSingle(ChestBlockEntity localChest) {
return Optional.of(localChest);
}
@Override
public Optional<MenuProvider> acceptNone() {
return Optional.empty();
}
});
if (menuOptional.isEmpty()) {
OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
return false;
}
menuProvider = menuOptional.get();
if (block instanceof TrappedChestBlock) {
bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED);
} else {
@@ -156,7 +217,7 @@ public class AnySilentContainer implements IAnySilentContainer {
if (blockEntity instanceof RandomizableContainerBlockEntity lootable) {
if (lootable.lootTable != null) {
lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
return false;
}
}
@@ -170,24 +231,31 @@ public class AnySilentContainer implements IAnySilentContainer {
@Override
public void deactivateContainer(@NotNull final Player bukkitPlayer) {
if (this.serverPlayerGameModeGameType == null || bukkitPlayer.getGameMode() == GameMode.SPECTATOR) {
if (this.serverPlayerGameModeGameType == null) {
return;
}
ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
InventoryView view = bukkitPlayer.getOpenInventory();
switch (view.getType()) {
case CHEST:
case ENDER_CHEST:
case SHULKER_BOX:
case BARREL:
break;
default:
return;
}
ServerPlayer player = PlayerDataManager.getHandle(bukkitPlayer);
// Force game mode change without informing plugins or players.
// Regular game mode set calls GameModeChangeEvent and is cancellable.
GameType gameType = player.gameMode.getGameModeForPlayer();
this.forceGameType(player, GameType.SPECTATOR);
// ServerPlayer#closeContainer cannot be called without entering an
// infinite loop because this method is called during inventory close.
// From ServerPlayer#closeContainer -> CraftEventFactory#handleInventoryCloseEvent
player.containerMenu.transferTo(player.inventoryMenu, player.getBukkitEntity());
// From ServerPlayer#closeContainer
// Close container - note that this is very different from ServerPlayer#closeContainer!
// Triggering event must not be re-called or we'll enter an infinite loop.
player.doCloseContainer();
// Regular inventory close will handle the rest - packet sending, etc.
// Revert forced game mode.
this.forceGameType(player, gameType);
@@ -202,7 +270,7 @@ public class AnySilentContainer implements IAnySilentContainer {
this.serverPlayerGameModeGameType.setAccessible(true);
this.serverPlayerGameModeGameType.set(player.gameMode, gameMode);
} catch (IllegalArgumentException | IllegalAccessException e) {
logger.log(java.util.logging.Level.WARNING, "Error bypassing GameModeChangeEvent", e);
e.printStackTrace();
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_17_R1;
import java.io.File;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.storage.PlayerDataStorage;
import org.apache.logging.log4j.LogManager;
import org.bukkit.craftbukkit.v1_17_R1.CraftServer;
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer;
public class OpenPlayer extends CraftPlayer {
public OpenPlayer(CraftServer server, ServerPlayer entity) {
super(server, entity);
}
@Override
public void loadData() {
// See CraftPlayer#loadData
CompoundTag loaded = this.server.getHandle().playerIo.load(this.getHandle());
if (loaded != null) {
readExtraData(loaded);
}
}
@Override
public void saveData() {
ServerPlayer player = this.getHandle();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try {
PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
CompoundTag playerData = player.saveWithoutId(new CompoundTag());
setExtraData(playerData);
if (!isOnline()) {
// Special case: save old vehicle data
CompoundTag oldData = worldNBTStorage.load(player);
if (oldData != null && oldData.contains("RootVehicle", 10)) {
// See net.minecraft.server.PlayerList#a(NetworkManager, EntityPlayer) and net.minecraft.server.EntityPlayer#b(NBTTagCompound)
playerData.put("RootVehicle", oldData.getCompound("RootVehicle"));
}
}
File file = File.createTempFile(player.getStringUUID() + "-", ".dat", worldNBTStorage.getPlayerDir());
NbtIo.writeCompressed(playerData, file);
File file1 = new File(worldNBTStorage.getPlayerDir(), player.getStringUUID() + ".dat");
File file2 = new File(worldNBTStorage.getPlayerDir(), player.getStringUUID() + ".dat_old");
Util.safeReplaceFile(file1, file, file2);
} catch (Exception e) {
LogManager.getLogger().warn("Failed to save player data for {}", player.getName().getString());
}
}
}
@@ -0,0 +1,230 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_17_R1;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.IPlayerDataManager;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.OpenInventoryView;
import com.mojang.authlib.GameProfile;
import java.lang.reflect.Field;
import java.util.logging.Logger;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.craftbukkit.v1_17_R1.CraftServer;
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_17_R1.event.CraftEventFactory;
import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftContainer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerDataManager implements IPlayerDataManager {
private @Nullable Field bukkitEntity;
public PlayerDataManager() {
try {
bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
} catch (NoSuchFieldException e) {
Logger logger = OpenInv.getPlugin(OpenInv.class).getLogger();
logger.warning("Unable to obtain field to inject custom save process - players' mounts may be deleted when loaded.");
logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
bukkitEntity = null;
}
}
public static @NotNull ServerPlayer getHandle(final Player player) {
if (player instanceof CraftPlayer) {
return ((CraftPlayer) player).getHandle();
}
Server server = player.getServer();
ServerPlayer nmsPlayer = null;
if (server instanceof CraftServer) {
nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getUniqueId());
}
if (nmsPlayer == null) {
// Could use reflection to examine fields, but it's honestly not worth the bother.
throw new RuntimeException("Unable to fetch EntityPlayer from provided Player implementation");
}
return nmsPlayer;
}
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
// Create a profile and entity to load the player data
// See net.minecraft.server.PlayerList#attemptLogin
GameProfile profile = new GameProfile(offline.getUniqueId(),
offline.getName() != null ? offline.getName() : offline.getUniqueId().toString());
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
if (worldServer == null) {
return null;
}
ServerPlayer entity = new ServerPlayer(server, worldServer, profile);
try {
injectPlayer(entity);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// Get the bukkit entity
Player target = entity.getBukkitEntity();
if (target != null) {
// Load data
target.loadData();
}
// Return the entity
return target;
}
void injectPlayer(ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(player.server.server, player));
}
@NotNull
@Override
public Player inject(@NotNull Player player) {
try {
ServerPlayer nmsPlayer = getHandle(player);
injectPlayer(nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
e.printStackTrace();
return player;
}
}
@Nullable
@Override
public InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.connection == null) {
return null;
}
InventoryView view = getView(player, inventory);
if (view == null) {
return player.openInventory(inventory.getBukkitInventory());
}
AbstractContainerMenu container = new CraftContainer(view, nmsPlayer, nmsPlayer.nextContainerCounter()) {
@Override
public MenuType<?> getType() {
return getContainers(inventory.getBukkitInventory().getSize());
}
};
container.setTitle(new TextComponent(view.getTitle()));
container = CraftEventFactory.callInventoryOpenEvent(nmsPlayer, container);
if (container == null) {
return null;
}
nmsPlayer.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(),
new TextComponent(container.getBukkitView().getTitle())));
nmsPlayer.containerMenu = container;
nmsPlayer.initMenu(container);
return container.getBukkitView();
}
private @Nullable InventoryView getView(Player player, ISpecialInventory inventory) {
if (inventory instanceof SpecialEnderChest) {
return new OpenInventoryView(player, inventory, "container.enderchest", "'s Ender Chest");
} else if (inventory instanceof SpecialPlayerInventory) {
return new OpenInventoryView(player, inventory, "container.player", "'s Inventory");
} else {
return null;
}
}
static @NotNull MenuType<?> getContainers(int inventorySize) {
return switch (inventorySize) {
case 9 -> MenuType.GENERIC_9x1;
case 18 -> MenuType.GENERIC_9x2;
case 36 -> MenuType.GENERIC_9x4; // PLAYER
case 41, 45 -> MenuType.GENERIC_9x5;
case 54 -> MenuType.GENERIC_9x6;
default -> MenuType.GENERIC_9x3; // Default 27-slot inventory
};
}
@Override
public int convertToPlayerSlot(InventoryView view, int rawSlot) {
int topSize = view.getTopInventory().getSize();
if (topSize <= rawSlot) {
// Slot is not inside special inventory, use Bukkit logic.
return view.convertSlot(rawSlot);
}
// Main inventory, slots 0-26 -> 9-35
if (rawSlot < 27) {
return rawSlot + 9;
}
// Hotbar, slots 27-35 -> 0-8
if (rawSlot < 36) {
return rawSlot - 27;
}
// Armor, slots 36-39 -> 39-36
if (rawSlot < 40) {
return 36 + (39 - rawSlot);
}
// Off hand
if (rawSlot == 40) {
return 40;
}
// Drop slots, "out of inventory"
return -1;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,9 +14,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_20_R3;
package com.lishid.openinv.internal.v1_17_R1;
import com.lishid.openinv.internal.ISpecialEnderChest;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
@@ -30,17 +33,13 @@ import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpecialEnderChest {
private final CraftInventory inventory;
@@ -49,9 +48,9 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
private boolean playerOnline;
public SpecialEnderChest(final org.bukkit.entity.Player player, final Boolean online) {
super(PlayerManager.getHandle(player));
super(PlayerDataManager.getHandle(player));
this.inventory = new CraftInventory(this);
this.owner = PlayerManager.getHandle(player);
this.owner = PlayerDataManager.getHandle(player);
this.playerOnline = online;
this.items = this.owner.getEnderChestInventory().items;
}
@@ -61,6 +60,11 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
return inventory;
}
@Override
public boolean isInUse() {
return !this.getViewers().isEmpty();
}
@Override
public void setPlayerOffline() {
this.playerOnline = false;
@@ -68,29 +72,18 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
@Override
public void setPlayerOnline(@NotNull final org.bukkit.entity.Player player) {
if (this.playerOnline) {
return;
if (!this.playerOnline) {
try {
this.owner = PlayerDataManager.getHandle(player);
PlayerEnderChestContainer enderChest = owner.getEnderChestInventory();
for (int i = 0; i < enderChest.getContainerSize(); ++i) {
enderChest.setItem(i, this.items.get(i));
}
this.items = enderChest.items;
enderChest.transaction.addAll(this.transaction);
} catch (Exception ignored) {}
this.playerOnline = true;
}
ServerPlayer offlinePlayer = this.owner;
ServerPlayer onlinePlayer = PlayerManager.getHandle(player);
// Set owner to new player.
this.owner = onlinePlayer;
// Set player's ender chest contents to our modified contents.
PlayerEnderChestContainer onlineEnderChest = onlinePlayer.getEnderChestInventory();
for (int i = 0; i < onlineEnderChest.getContainerSize(); ++i) {
onlineEnderChest.setItem(i, this.items.get(i));
}
// Set our item array to the new inventory's array.
this.items = onlineEnderChest.items;
// Add viewers to new inventory.
onlineEnderChest.transaction.addAll(offlinePlayer.getEnderChestInventory().transaction);
this.playerOnline = true;
}
@Override
@@ -198,7 +191,7 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
@Override
public boolean canAddItem(ItemStack itemstack) {
for (ItemStack itemstack1 : this.items) {
if (itemstack1.isEmpty() || (ItemStack.isSameItemSameTags(itemstack1, itemstack) && itemstack1.getCount() < itemstack1.getMaxStackSize())) {
if (itemstack1.isEmpty() || ItemStack.isSameItemSameTags(itemstack1, itemstack) && itemstack1.getCount() < itemstack1.getMaxStackSize()) {
return true;
}
}
@@ -329,7 +322,7 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
@Override
public String toString() {
return this.items.stream().filter((itemStack) -> !itemStack.isEmpty()).toList().toString();
return this.items.stream().filter((itemStack) -> !itemStack.isEmpty()).collect(Collectors.toList()).toString();
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,10 +14,15 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_20_R3;
package com.lishid.openinv.internal.v1_17_R1;
import com.google.common.collect.ImmutableList;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
@@ -27,8 +32,7 @@ import net.minecraft.nbt.ListTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.DamageTypeTags;
import net.minecraft.tags.TagKey;
import net.minecraft.tags.Tag;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.damagesource.DamageSource;
@@ -41,20 +45,13 @@ import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_17_R1.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_17_R1.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerInventory {
private final CraftInventory inventory;
@@ -66,7 +63,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
private List<NonNullList<ItemStack>> compartments;
public SpecialPlayerInventory(@NotNull org.bukkit.entity.Player bukkitPlayer, @NotNull Boolean online) {
super(PlayerManager.getHandle(bukkitPlayer));
super(PlayerDataManager.getHandle(bukkitPlayer));
this.inventory = new CraftInventory(this);
this.playerOnline = online;
this.player = super.player;
@@ -79,37 +76,20 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
@Override
public void setPlayerOnline(@NotNull org.bukkit.entity.Player player) {
if (this.playerOnline) {
return;
if (!this.playerOnline) {
Player entityPlayer = PlayerDataManager.getHandle(player);
entityPlayer.getInventory().transaction.addAll(this.transaction);
this.player = entityPlayer;
for (int i = 0; i < getContainerSize(); ++i) {
this.player.getInventory().setItem(i, getRawItem(i));
}
this.player.getInventory().selected = this.selected;
this.items = this.player.getInventory().items;
this.armor = this.player.getInventory().armor;
this.offhand = this.player.getInventory().offhand;
this.compartments = ImmutableList.of(this.items, this.armor, this.offhand);
this.playerOnline = true;
}
Player offlinePlayer = this.player;
Player onlinePlayer = PlayerManager.getHandle(player);
onlinePlayer.getInventory().transaction.addAll(this.transaction);
// Set owner to new player.
this.player = onlinePlayer;
// Set player's inventory contents to our modified contents.
Inventory onlineInventory = onlinePlayer.getInventory();
for (int i = 0; i < getContainerSize(); ++i) {
onlineInventory.setItem(i, getRawItem(i));
}
onlineInventory.selected = this.selected;
// Set our item arrays to the new inventory's arrays.
this.items = onlineInventory.items;
this.armor = onlineInventory.armor;
this.offhand = onlineInventory.offhand;
this.compartments = ImmutableList.of(this.items, this.armor, this.offhand);
// Add existing viewers to new viewer list.
Inventory offlineInventory = offlinePlayer.getInventory();
// Remove self from listing - player is always a viewer of their own inventory, prevent duplicates.
offlineInventory.transaction.remove(offlinePlayer.getBukkitEntity());
onlineInventory.transaction.addAll(offlineInventory.transaction);
this.playerOnline = true;
}
@Override
@@ -122,42 +102,25 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
this.playerOnline = false;
}
@Override
public boolean isInUse() {
return !this.getViewers().isEmpty();
}
@Override
public @NotNull HumanEntity getPlayer() {
return this.player.getBukkitEntity();
}
private @NotNull ItemStack getRawItem(int i) {
if (i < 0) {
return ItemStack.EMPTY;
}
NonNullList<ItemStack> list;
for (Iterator<NonNullList<ItemStack>> iterator = this.compartments.iterator(); iterator.hasNext(); i -= list.size()) {
list = iterator.next();
if (i < list.size()) {
return list.get(i);
}
}
return ItemStack.EMPTY;
return super.getItem(i);
}
private void setRawItem(int i, @NotNull ItemStack itemStack) {
if (i < 0) {
return;
}
NonNullList<ItemStack> list;
for (Iterator<NonNullList<ItemStack>> iterator = this.compartments.iterator(); iterator.hasNext(); i -= list.size()) {
list = iterator.next();
if (i < list.size()) {
list.set(i, itemStack);
}
}
super.setItem(i, itemStack);
}
private record IndexedCompartment(@Nullable NonNullList<ItemStack> compartment, int index) {}
private static record IndexedCompartment(@Nullable NonNullList<ItemStack> compartment, int index) {}
private @NotNull SpecialPlayerInventory.IndexedCompartment getIndexedContent(int index) {
if (index < items.size()) {
@@ -475,7 +438,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
for (NonNullList<ItemStack> compartment : this.compartments) {
for (int i = 0; i < compartment.size(); ++i) {
if (!compartment.get(i).isEmpty()) {
compartment.get(i).inventoryTick(this.player.level(), this.player, i, this.selected == i);
compartment.get(i).inventoryTick(this.player.level, this.player, i, this.selected == i);
}
}
}
@@ -721,7 +684,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
for (int index : armorIndices) {
ItemStack itemstack = this.armor.get(index);
if ((!damagesource.is(DamageTypeTags.IS_FIRE) || !itemstack.getItem().isFireResistant()) && itemstack.getItem() instanceof ArmorItem) {
if ((!damagesource.isFire() || !itemstack.getItem().isFireResistant()) && itemstack.getItem() instanceof ArmorItem) {
itemstack.hurtAndBreak((int) damage, this.player, localPlayer -> localPlayer.broadcastBreakEvent(EquipmentSlot.byTypeAndIndex(EquipmentSlot.Type.ARMOR, index)));
}
}
@@ -758,13 +721,12 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
@Override
public boolean contains(ItemStack itemstack) {
return contains(itemStack -> !itemStack.isEmpty() && itemStack.is(itemstack.getItem()));
return contains(itemStack -> itemStack.isEmpty() && itemStack.sameItem(itemstack));
}
@Override
public boolean contains(TagKey<Item> tagKey) {
return contains(itemStack -> !itemStack.isEmpty() && itemStack.is(tagKey));
public boolean contains(Tag<Item> tag) {
return contains(itemStack -> !itemStack.isEmpty() && itemStack.is(tag));
}
@Override
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2011-2023 lishid. All rights reserved.
~ Copyright (C) 2011-2021 lishid. All rights reserved.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
@@ -23,16 +23,16 @@
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<relativePath>../../pom.xml</relativePath>
<version>5.0.0</version>
<version>4.1.9-SNAPSHOT</version>
</parent>
<artifactId>openinvadapter1_20_R3</artifactId>
<name>OpenInvAdapter1_20_R3</name>
<artifactId>openinvadapter1_18_R1</artifactId>
<name>OpenInvAdapter1_18_R1</name>
<properties>
<maven.compiler.system>17</maven.compiler.system>
<maven.compiler.release>17</maven.compiler.release>
<spigot.version>1.20.4-R0.1-SNAPSHOT</spigot.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spigot.version>1.18-pre5-R0.1-SNAPSHOT</spigot.version>
</properties>
<dependencies>
@@ -53,7 +53,7 @@
<groupId>com.lishid</groupId>
</dependency>
<dependency>
<artifactId>openinvcommon</artifactId>
<artifactId>openinvplugincore</artifactId>
<groupId>com.lishid</groupId>
</dependency>
<dependency>
@@ -64,12 +64,46 @@
<build>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<configuration>
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>net.md-5</groupId>
<artifactId>specialsource-maven-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>remap</goal>
</goals>
<id>remap-obf</id>
<configuration>
<srgIn>org.spigotmc:minecraft-server:${spigot.version}:txt:maps-mojang</srgIn>
<reverse>true</reverse>
<remappedDependencies>org.spigotmc:spigot:${spigot.version}:jar:remapped-mojang</remappedDependencies>
<remappedArtifactAttached>true</remappedArtifactAttached>
<remappedClassifierName>remapped-obf</remappedClassifierName>
</configuration>
</execution>
<execution>
<phase>package</phase>
<goals>
<goal>remap</goal>
</goals>
<id>remap-spigot</id>
<configuration>
<inputFile>${project.build.directory}/${project.artifactId}-${project.version}-remapped-obf.jar</inputFile>
<srgIn>org.spigotmc:minecraft-server:${spigot.version}:csrg:maps-spigot</srgIn>
<remappedDependencies>org.spigotmc:spigot:${spigot.version}:jar:remapped-obf</remappedDependencies>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,17 +14,24 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_20_R4;
package com.lishid.openinv.internal.v1_18_R1;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.util.ReflectionHelper;
import com.lishid.openinv.util.lang.LanguageManager;
import java.lang.reflect.Field;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.world.CompoundContainer;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.entity.monster.Shulker;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.PlayerEnderChestContainer;
@@ -32,36 +39,39 @@ import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.BarrelBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.DoubleBlockCombiner;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.TrappedChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.ChestBlockEntity;
import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.entity.ShulkerBoxBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.GameMode;
import net.minecraft.world.phys.AABB;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.block.ShulkerBox;
import org.bukkit.craftbukkit.v1_18_R1.CraftWorld;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.logging.Logger;
public class AnySilentContainer implements IAnySilentContainer {
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
private @Nullable Field serverPlayerGameModeGameType;
public AnySilentContainer(@NotNull Logger logger, @NotNull LanguageManager lang) {
this.logger = logger;
this.lang = lang;
public AnySilentContainer() {
try {
try {
// IDE warns about field not existing, but SpecialSource does not remap strings used in reflection.
// The warning is not suppressed as a reminder that it must manually be checked on updates.
this.serverPlayerGameModeGameType = ServerPlayerGameMode.class.getDeclaredField("b");
this.serverPlayerGameModeGameType.setAccessible(true);
} catch (NoSuchFieldException e) {
Logger logger = OpenInv.getPlugin(OpenInv.class).getLogger();
logger.warning("ServerPlayerGameMode#gameModeForPlayer's obfuscated name has changed!");
logger.warning("Please report this at https://github.com/Jikoo/OpenInv/issues");
logger.warning("Attempting to fall through using reflection. Please verify that SilentContainer does not fail.");
@@ -69,11 +79,43 @@ public class AnySilentContainer implements IAnySilentContainer {
this.serverPlayerGameModeGameType = ReflectionHelper.grabFieldByType(ServerPlayerGameMode.class, GameType.class);
}
} catch (SecurityException e) {
Logger logger = OpenInv.getPlugin(OpenInv.class).getLogger();
logger.warning("Unable to directly write player game mode! SilentContainer will fail.");
logger.log(java.util.logging.Level.WARNING, "Error obtaining GameType field", e);
logger.log(Level.WARNING, "Error obtaining GameType field", e);
}
}
@Override
public boolean isShulkerBlocked(ShulkerBox box) {
org.bukkit.World bukkitWorld = box.getWorld();
if (!(bukkitWorld instanceof CraftWorld)) {
bukkitWorld = Bukkit.getWorld(bukkitWorld.getUID());
}
if (!(bukkitWorld instanceof CraftWorld craftWorld)) {
Exception exception = new IllegalStateException("AnySilentContainer access attempted on an unknown world!");
OpenInv.getPlugin(OpenInv.class).getLogger().log(Level.WARNING, exception.getMessage(), exception);
return false;
}
final ServerLevel world = craftWorld.getHandle();
final BlockPos blockPosition = new BlockPos(box.getX(), box.getY(), box.getZ());
final BlockEntity tile = world.getBlockEntity(blockPosition);
if (!(tile instanceof ShulkerBoxBlockEntity shulkerBoxBlockEntity)
|| shulkerBoxBlockEntity.getAnimationStatus() != ShulkerBoxBlockEntity.AnimationStatus.CLOSED) {
return false;
}
BlockState blockState = world.getBlockState(blockPosition);
// See net.minecraft.world.level.block.ShulkerBoxBlock#canOpen
AABB boundingBox = Shulker.getProgressDeltaAabb(blockState.getValue(ShulkerBoxBlock.FACING), 0.0F, 0.5F)
.move(blockPosition)
.deflate(1.0E-6D);
return !world.noCollision(boundingBox);
}
@Override
public boolean activateContainer(
@NotNull final Player bukkitPlayer,
@@ -87,9 +129,9 @@ public class AnySilentContainer implements IAnySilentContainer {
return true;
}
ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
ServerPlayer player = PlayerDataManager.getHandle(bukkitPlayer);
final net.minecraft.world.level.Level level = player.level();
final ServerLevel level = player.getLevel();
final BlockPos blockPos = new BlockPos(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ());
final BlockEntity blockEntity = level.getBlockEntity(blockPos);
@@ -98,14 +140,14 @@ public class AnySilentContainer implements IAnySilentContainer {
}
if (blockEntity instanceof EnderChestBlockEntity enderChestTile) {
// Anychest ender chest. See net.minecraft.world.level.block.EnderChestBlock
// Anychest ender chest. See net.minecraft.world.level.block.BlockEnderChest
PlayerEnderChestContainer enderChest = player.getEnderChestInventory();
enderChest.setActiveChest(enderChestTile);
player.openMenu(new SimpleMenuProvider((containerCounter, playerInventory, ignored) -> {
MenuType<?> containers = PlayerManager.getContainers(enderChest.getContainerSize());
MenuType<?> containers = PlayerDataManager.getContainers(enderChest.getContainerSize());
int rows = enderChest.getContainerSize() / 9;
return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows);
}, Component.translatable("container.enderchest")));
}, new TextComponent("container.enderchest")));
bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
return true;
}
@@ -119,14 +161,34 @@ public class AnySilentContainer implements IAnySilentContainer {
if (block instanceof ChestBlock chestBlock) {
// boolean flag: do not check if chest is blocked
menuProvider = chestBlock.getMenuProvider(blockState, level, blockPos, true);
// boolean flag: check if chest is blocked
Optional<MenuProvider> menuOptional = chestBlock.combine(blockState, level, blockPos, false).apply(
// Combiner is a copy of private ChestBlock.MENU_PROVIDER_COMBINER
new DoubleBlockCombiner.Combiner<ChestBlockEntity, Optional<MenuProvider>>() {
@Override
public Optional<MenuProvider> acceptDouble(ChestBlockEntity localChest1, ChestBlockEntity localChest2) {
CompoundContainer doubleChest = new CompoundContainer(localChest1, localChest2);
return Optional.of(new ChestBlock.DoubleInventory(localChest1, localChest2, doubleChest));
}
if (menuProvider == null) {
lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
@Override
public Optional<MenuProvider> acceptSingle(ChestBlockEntity localChest) {
return Optional.of(localChest);
}
@Override
public Optional<MenuProvider> acceptNone() {
return Optional.empty();
}
});
if (menuOptional.isEmpty()) {
OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
return false;
}
menuProvider = menuOptional.get();
if (block instanceof TrappedChestBlock) {
bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED);
} else {
@@ -155,7 +217,7 @@ public class AnySilentContainer implements IAnySilentContainer {
if (blockEntity instanceof RandomizableContainerBlockEntity lootable) {
if (lootable.lootTable != null) {
lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
OpenInv.getPlugin(OpenInv.class).sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
return false;
}
}
@@ -169,24 +231,31 @@ public class AnySilentContainer implements IAnySilentContainer {
@Override
public void deactivateContainer(@NotNull final Player bukkitPlayer) {
if (this.serverPlayerGameModeGameType == null || bukkitPlayer.getGameMode() == GameMode.SPECTATOR) {
if (this.serverPlayerGameModeGameType == null) {
return;
}
ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
InventoryView view = bukkitPlayer.getOpenInventory();
switch (view.getType()) {
case CHEST:
case ENDER_CHEST:
case SHULKER_BOX:
case BARREL:
break;
default:
return;
}
ServerPlayer player = PlayerDataManager.getHandle(bukkitPlayer);
// Force game mode change without informing plugins or players.
// Regular game mode set calls GameModeChangeEvent and is cancellable.
GameType gameType = player.gameMode.getGameModeForPlayer();
this.forceGameType(player, GameType.SPECTATOR);
// ServerPlayer#closeContainer cannot be called without entering an
// infinite loop because this method is called during inventory close.
// From ServerPlayer#closeContainer -> CraftEventFactory#handleInventoryCloseEvent
player.containerMenu.transferTo(player.inventoryMenu, player.getBukkitEntity());
// From ServerPlayer#closeContainer
// Close container - note that this is very different from ServerPlayer#closeContainer!
// Triggering event must not be re-called or we'll enter an infinite loop.
player.doCloseContainer();
// Regular inventory close will handle the rest - packet sending, etc.
// Revert forced game mode.
this.forceGameType(player, gameType);
@@ -201,7 +270,7 @@ public class AnySilentContainer implements IAnySilentContainer {
this.serverPlayerGameModeGameType.setAccessible(true);
this.serverPlayerGameModeGameType.set(player.gameMode, gameMode);
} catch (IllegalArgumentException | IllegalAccessException e) {
logger.log(java.util.logging.Level.WARNING, "Error bypassing GameModeChangeEvent", e);
e.printStackTrace();
}
}
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_18_R1;
import java.io.File;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.storage.PlayerDataStorage;
import org.apache.logging.log4j.LogManager;
import org.bukkit.craftbukkit.v1_18_R1.CraftServer;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer;
public class OpenPlayer extends CraftPlayer {
public OpenPlayer(CraftServer server, ServerPlayer entity) {
super(server, entity);
}
@Override
public void loadData() {
// See CraftPlayer#loadData
CompoundTag loaded = this.server.getHandle().playerIo.load(this.getHandle());
if (loaded != null) {
readExtraData(loaded);
}
}
@Override
public void saveData() {
ServerPlayer player = this.getHandle();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try {
PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
CompoundTag playerData = player.saveWithoutId(new CompoundTag());
setExtraData(playerData);
if (!isOnline()) {
// Special case: save old vehicle data
CompoundTag oldData = worldNBTStorage.load(player);
if (oldData != null && oldData.contains("RootVehicle", 10)) {
// See net.minecraft.server.PlayerList#a(NetworkManager, EntityPlayer) and net.minecraft.server.EntityPlayer#b(NBTTagCompound)
playerData.put("RootVehicle", oldData.getCompound("RootVehicle"));
}
}
File file = File.createTempFile(player.getStringUUID() + "-", ".dat", worldNBTStorage.getPlayerDir());
NbtIo.writeCompressed(playerData, file);
File file1 = new File(worldNBTStorage.getPlayerDir(), player.getStringUUID() + ".dat");
File file2 = new File(worldNBTStorage.getPlayerDir(), player.getStringUUID() + ".dat_old");
Util.safeReplaceFile(file1, file, file2);
} catch (Exception e) {
LogManager.getLogger().warn("Failed to save player data for {}", player.getName().getString());
}
}
}
@@ -0,0 +1,230 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_18_R1;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.IPlayerDataManager;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.OpenInventoryView;
import com.mojang.authlib.GameProfile;
import java.lang.reflect.Field;
import java.util.logging.Logger;
import net.minecraft.network.chat.TextComponent;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.craftbukkit.v1_18_R1.CraftServer;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_18_R1.event.CraftEventFactory;
import org.bukkit.craftbukkit.v1_18_R1.inventory.CraftContainer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerDataManager implements IPlayerDataManager {
private @Nullable Field bukkitEntity;
public PlayerDataManager() {
try {
bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
} catch (NoSuchFieldException e) {
Logger logger = OpenInv.getPlugin(OpenInv.class).getLogger();
logger.warning("Unable to obtain field to inject custom save process - players' mounts may be deleted when loaded.");
logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
bukkitEntity = null;
}
}
public static @NotNull ServerPlayer getHandle(final Player player) {
if (player instanceof CraftPlayer) {
return ((CraftPlayer) player).getHandle();
}
Server server = player.getServer();
ServerPlayer nmsPlayer = null;
if (server instanceof CraftServer) {
nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getUniqueId());
}
if (nmsPlayer == null) {
// Could use reflection to examine fields, but it's honestly not worth the bother.
throw new RuntimeException("Unable to fetch EntityPlayer from provided Player implementation");
}
return nmsPlayer;
}
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
// Create a profile and entity to load the player data
// See net.minecraft.server.PlayerList#attemptLogin
GameProfile profile = new GameProfile(offline.getUniqueId(),
offline.getName() != null ? offline.getName() : offline.getUniqueId().toString());
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
if (worldServer == null) {
return null;
}
ServerPlayer entity = new ServerPlayer(server, worldServer, profile);
try {
injectPlayer(entity);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// Get the bukkit entity
Player target = entity.getBukkitEntity();
if (target != null) {
// Load data
target.loadData();
}
// Return the entity
return target;
}
void injectPlayer(ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(player.server.server, player));
}
@NotNull
@Override
public Player inject(@NotNull Player player) {
try {
ServerPlayer nmsPlayer = getHandle(player);
injectPlayer(nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
e.printStackTrace();
return player;
}
}
@Nullable
@Override
public InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.connection == null) {
return null;
}
InventoryView view = getView(player, inventory);
if (view == null) {
return player.openInventory(inventory.getBukkitInventory());
}
AbstractContainerMenu container = new CraftContainer(view, nmsPlayer, nmsPlayer.nextContainerCounter()) {
@Override
public MenuType<?> getType() {
return getContainers(inventory.getBukkitInventory().getSize());
}
};
container.setTitle(new TextComponent(view.getTitle()));
container = CraftEventFactory.callInventoryOpenEvent(nmsPlayer, container);
if (container == null) {
return null;
}
nmsPlayer.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(),
new TextComponent(container.getBukkitView().getTitle())));
nmsPlayer.containerMenu = container;
nmsPlayer.initMenu(container);
return container.getBukkitView();
}
private @Nullable InventoryView getView(Player player, ISpecialInventory inventory) {
if (inventory instanceof SpecialEnderChest) {
return new OpenInventoryView(player, inventory, "container.enderchest", "'s Ender Chest");
} else if (inventory instanceof SpecialPlayerInventory) {
return new OpenInventoryView(player, inventory, "container.player", "'s Inventory");
} else {
return null;
}
}
static @NotNull MenuType<?> getContainers(int inventorySize) {
return switch (inventorySize) {
case 9 -> MenuType.GENERIC_9x1;
case 18 -> MenuType.GENERIC_9x2;
case 36 -> MenuType.GENERIC_9x4; // PLAYER
case 41, 45 -> MenuType.GENERIC_9x5;
case 54 -> MenuType.GENERIC_9x6;
default -> MenuType.GENERIC_9x3; // Default 27-slot inventory
};
}
@Override
public int convertToPlayerSlot(InventoryView view, int rawSlot) {
int topSize = view.getTopInventory().getSize();
if (topSize <= rawSlot) {
// Slot is not inside special inventory, use Bukkit logic.
return view.convertSlot(rawSlot);
}
// Main inventory, slots 0-26 -> 9-35
if (rawSlot < 27) {
return rawSlot + 9;
}
// Hotbar, slots 27-35 -> 0-8
if (rawSlot < 36) {
return rawSlot - 27;
}
// Armor, slots 36-39 -> 39-36
if (rawSlot < 40) {
return 36 + (39 - rawSlot);
}
// Off hand
if (rawSlot == 40) {
return 40;
}
// Drop slots, "out of inventory"
return -1;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,10 +14,12 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_20_R4;
package com.lishid.openinv.internal.v1_18_R1;
import com.lishid.openinv.internal.ISpecialEnderChest;
import net.minecraft.core.HolderLookup;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.core.NonNullList;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
@@ -31,17 +33,13 @@ import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_18_R1.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpecialEnderChest {
private final CraftInventory inventory;
@@ -50,9 +48,9 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
private boolean playerOnline;
public SpecialEnderChest(final org.bukkit.entity.Player player, final Boolean online) {
super(PlayerManager.getHandle(player));
super(PlayerDataManager.getHandle(player));
this.inventory = new CraftInventory(this);
this.owner = PlayerManager.getHandle(player);
this.owner = PlayerDataManager.getHandle(player);
this.playerOnline = online;
this.items = this.owner.getEnderChestInventory().items;
}
@@ -62,6 +60,11 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
return inventory;
}
@Override
public boolean isInUse() {
return !this.getViewers().isEmpty();
}
@Override
public void setPlayerOffline() {
this.playerOnline = false;
@@ -69,29 +72,18 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
@Override
public void setPlayerOnline(@NotNull final org.bukkit.entity.Player player) {
if (this.playerOnline) {
return;
if (!this.playerOnline) {
try {
this.owner = PlayerDataManager.getHandle(player);
PlayerEnderChestContainer enderChest = owner.getEnderChestInventory();
for (int i = 0; i < enderChest.getContainerSize(); ++i) {
enderChest.setItem(i, this.items.get(i));
}
this.items = enderChest.items;
enderChest.transaction.addAll(this.transaction);
} catch (Exception ignored) {}
this.playerOnline = true;
}
ServerPlayer offlinePlayer = this.owner;
ServerPlayer onlinePlayer = PlayerManager.getHandle(player);
// Set owner to new player.
this.owner = onlinePlayer;
// Set player's ender chest contents to our modified contents.
PlayerEnderChestContainer onlineEnderChest = onlinePlayer.getEnderChestInventory();
for (int i = 0; i < onlineEnderChest.getContainerSize(); ++i) {
onlineEnderChest.setItem(i, this.items.get(i));
}
// Set our item array to the new inventory's array.
this.items = onlineEnderChest.items;
// Add viewers to new inventory.
onlineEnderChest.transaction.addAll(offlinePlayer.getEnderChestInventory().transaction);
this.playerOnline = true;
}
@Override
@@ -199,7 +191,7 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
@Override
public boolean canAddItem(ItemStack itemstack) {
for (ItemStack itemstack1 : this.items) {
if (itemstack1.isEmpty() || (ItemStack.isSameItemSameComponents(itemstack1, itemstack) && itemstack1.getCount() < itemstack1.getMaxStackSize())) {
if (itemstack1.isEmpty() || ItemStack.isSameItemSameTags(itemstack1, itemstack) && itemstack1.getCount() < itemstack1.getMaxStackSize()) {
return true;
}
}
@@ -221,7 +213,7 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
private void moveItemToOccupiedSlotsWithSameType(ItemStack itemstack) {
for(int i = 0; i < this.getContainerSize(); ++i) {
ItemStack localItem = this.getItem(i);
if (ItemStack.isSameItemSameComponents(localItem, itemstack)) {
if (ItemStack.isSameItemSameTags(localItem, itemstack)) {
this.moveItemsBetweenStacks(itemstack, localItem);
if (itemstack.isEmpty()) {
return;
@@ -330,11 +322,11 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
@Override
public String toString() {
return this.items.stream().filter((itemStack) -> !itemStack.isEmpty()).toList().toString();
return this.items.stream().filter((itemStack) -> !itemStack.isEmpty()).collect(Collectors.toList()).toString();
}
@Override
public void fromTag(ListTag listTag, HolderLookup.Provider holderLookup) {
public void fromTag(ListTag listTag) {
for (int i = 0; i < this.getContainerSize(); ++i) {
this.setItem(i, ItemStack.EMPTY);
}
@@ -343,7 +335,7 @@ public class SpecialEnderChest extends PlayerEnderChestContainer implements ISpe
CompoundTag compoundTag = listTag.getCompound(i);
int j = compoundTag.getByte("Slot") & 255;
if (j < this.getContainerSize()) {
this.setItem(j, ItemStack.parseOptional(holderLookup, compoundTag));
this.setItem(j, ItemStack.of(compoundTag));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2023 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,44 +14,44 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.internal.v1_20_R4;
package com.lishid.openinv.internal.v1_18_R1;
import com.google.common.collect.ImmutableList;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import net.minecraft.CrashReport;
import net.minecraft.CrashReportCategory;
import net.minecraft.ReportedException;
import net.minecraft.core.NonNullList;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.tags.TagKey;
import net.minecraft.tags.Tag;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.EquipmentSlot;
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.item.ArmorItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_18_R1.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_18_R1.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerInventory {
private final CraftInventory inventory;
@@ -63,7 +63,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
private List<NonNullList<ItemStack>> compartments;
public SpecialPlayerInventory(@NotNull org.bukkit.entity.Player bukkitPlayer, @NotNull Boolean online) {
super(PlayerManager.getHandle(bukkitPlayer));
super(PlayerDataManager.getHandle(bukkitPlayer));
this.inventory = new CraftInventory(this);
this.playerOnline = online;
this.player = super.player;
@@ -76,37 +76,20 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
@Override
public void setPlayerOnline(@NotNull org.bukkit.entity.Player player) {
if (this.playerOnline) {
return;
if (!this.playerOnline) {
Player entityPlayer = PlayerDataManager.getHandle(player);
entityPlayer.getInventory().transaction.addAll(this.transaction);
this.player = entityPlayer;
for (int i = 0; i < getContainerSize(); ++i) {
this.player.getInventory().setItem(i, getRawItem(i));
}
this.player.getInventory().selected = this.selected;
this.items = this.player.getInventory().items;
this.armor = this.player.getInventory().armor;
this.offhand = this.player.getInventory().offhand;
this.compartments = ImmutableList.of(this.items, this.armor, this.offhand);
this.playerOnline = true;
}
Player offlinePlayer = this.player;
Player onlinePlayer = PlayerManager.getHandle(player);
onlinePlayer.getInventory().transaction.addAll(this.transaction);
// Set owner to new player.
this.player = onlinePlayer;
// Set player's inventory contents to our modified contents.
Inventory onlineInventory = onlinePlayer.getInventory();
for (int i = 0; i < getContainerSize(); ++i) {
onlineInventory.setItem(i, getRawItem(i));
}
onlineInventory.selected = this.selected;
// Set our item arrays to the new inventory's arrays.
this.items = onlineInventory.items;
this.armor = onlineInventory.armor;
this.offhand = onlineInventory.offhand;
this.compartments = ImmutableList.of(this.items, this.armor, this.offhand);
// Add existing viewers to new viewer list.
Inventory offlineInventory = offlinePlayer.getInventory();
// Remove self from listing - player is always a viewer of their own inventory, prevent duplicates.
offlineInventory.transaction.remove(offlinePlayer.getBukkitEntity());
onlineInventory.transaction.addAll(offlineInventory.transaction);
this.playerOnline = true;
}
@Override
@@ -119,42 +102,25 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
this.playerOnline = false;
}
@Override
public boolean isInUse() {
return !this.getViewers().isEmpty();
}
@Override
public @NotNull HumanEntity getPlayer() {
return this.player.getBukkitEntity();
}
private @NotNull ItemStack getRawItem(int i) {
if (i < 0) {
return ItemStack.EMPTY;
}
NonNullList<ItemStack> list;
for (Iterator<NonNullList<ItemStack>> iterator = this.compartments.iterator(); iterator.hasNext(); i -= list.size()) {
list = iterator.next();
if (i < list.size()) {
return list.get(i);
}
}
return ItemStack.EMPTY;
return super.getItem(i);
}
private void setRawItem(int i, @NotNull ItemStack itemStack) {
if (i < 0) {
return;
}
NonNullList<ItemStack> list;
for (Iterator<NonNullList<ItemStack>> iterator = this.compartments.iterator(); iterator.hasNext(); i -= list.size()) {
list = iterator.next();
if (i < list.size()) {
list.set(i, itemStack);
}
}
super.setItem(i, itemStack);
}
private record IndexedCompartment(@Nullable NonNullList<ItemStack> compartment, int index) {}
private static record IndexedCompartment(@Nullable NonNullList<ItemStack> compartment, int index) {}
private @NotNull SpecialPlayerInventory.IndexedCompartment getIndexedContent(int index) {
if (index < items.size()) {
@@ -201,8 +167,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
return i + 9;
}
@Override
public boolean contains(Predicate<ItemStack> predicate) {
private boolean contains(Predicate<ItemStack> predicate) {
return this.compartments.stream().flatMap(NonNullList::stream).anyMatch(predicate);
}
@@ -262,7 +227,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
}
private boolean hasRemainingSpaceForItem(ItemStack itemstack, ItemStack itemstack1) {
return !itemstack.isEmpty() && ItemStack.isSameItemSameComponents(itemstack, itemstack1) && itemstack.isStackable() && itemstack.getCount() < itemstack.getMaxStackSize() && itemstack.getCount() < this.getMaxStackSize();
return !itemstack.isEmpty() && ItemStack.isSameItemSameTags(itemstack, itemstack1) && itemstack.isStackable() && itemstack.getCount() < itemstack.getMaxStackSize() && itemstack.getCount() < this.getMaxStackSize();
}
@Override
@@ -335,7 +300,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
@Override
public int findSlotMatchingItem(ItemStack itemstack) {
for(int i = 0; i < this.items.size(); ++i) {
if (!this.items.get(i).isEmpty() && ItemStack.isSameItemSameComponents(itemstack, this.items.get(i))) {
if (!this.items.get(i).isEmpty() && ItemStack.isSameItemSameTags(itemstack, this.items.get(i))) {
return i;
}
}
@@ -347,7 +312,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
public int findSlotMatchingUnusedItem(ItemStack itemStack) {
for(int i = 0; i < this.items.size(); ++i) {
ItemStack localItem = this.items.get(i);
if (!this.items.get(i).isEmpty() && ItemStack.isSameItemSameComponents(itemStack, this.items.get(i)) && !this.items.get(i).isDamaged() && !localItem.isEnchanted() && !localItem.has(DataComponents.CUSTOM_NAME)) {
if (!this.items.get(i).isEmpty() && ItemStack.isSameItemSameTags(itemStack, this.items.get(i)) && !this.items.get(i).isDamaged() && !localItem.isEnchanted() && !localItem.hasCustomHoverName()) {
return i;
}
}
@@ -378,9 +343,15 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
@Override
public void swapPaint(double d0) {
int i = (int) Math.signum(d0);
if (d0 > 0.0D) {
d0 = 1.0D;
}
this.selected -= i;
if (d0 < 0.0D) {
d0 = -1.0D;
}
this.selected = (int) (this.selected - d0);
while (this.selected < 0) {
this.selected += 9;
@@ -416,14 +387,25 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
}
private int addResource(int i, ItemStack itemstack) {
Item item = itemstack.getItem();
int j = itemstack.getCount();
ItemStack localItemStack = this.getRawItem(i);
if (localItemStack.isEmpty()) {
localItemStack = itemstack.copyWithCount(0);
localItemStack = new ItemStack(item, 0);
if (itemstack.hasTag()) {
// hasTag ensures tag not null
//noinspection ConstantConditions
localItemStack.setTag(itemstack.getTag().copy());
}
this.setRawItem(i, localItemStack);
}
int k = Math.min(j, this.getMaxStackSize(localItemStack) - localItemStack.getCount());
int k = Math.min(j, localItemStack.getMaxStackSize() - localItemStack.getCount());
if (k > this.getMaxStackSize() - localItemStack.getCount()) {
k = this.getMaxStackSize() - localItemStack.getCount();
}
if (k != 0) {
j -= k;
@@ -456,7 +438,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
for (NonNullList<ItemStack> compartment : this.compartments) {
for (int i = 0; i < compartment.size(); ++i) {
if (!compartment.get(i).isEmpty()) {
compartment.get(i).inventoryTick(this.player.level(), this.player, i, this.selected == i);
compartment.get(i).inventoryTick(this.player.level, this.player, i, this.selected == i);
}
}
}
@@ -484,7 +466,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
this.items.get(i).setPopTime(5);
itemStack.setCount(0);
return true;
} else if (this.player.hasInfiniteMaterials()) {
} else if (this.player.getAbilities().instabuild) {
itemStack.setCount(0);
return true;
} else {
@@ -501,7 +483,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
}
} while(!itemStack.isEmpty() && itemStack.getCount() < j);
if (itemStack.getCount() == j && this.player.hasInfiniteMaterials()) {
if (itemStack.getCount() == j && this.player.getAbilities().instabuild) {
itemStack.setCount(0);
return true;
} else {
@@ -612,7 +594,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
if (!this.items.get(i).isEmpty()) {
CompoundTag compoundTag = new CompoundTag();
compoundTag.putByte("Slot", (byte)i);
this.items.get(i).save(this.player.registryAccess(), compoundTag);
this.items.get(i).save(compoundTag);
listTag.add(compoundTag);
}
}
@@ -621,7 +603,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
if (!this.armor.get(i).isEmpty()) {
CompoundTag compoundTag = new CompoundTag();
compoundTag.putByte("Slot", (byte)(i + 100));
this.armor.get(i).save(this.player.registryAccess(), compoundTag);
this.armor.get(i).save(compoundTag);
listTag.add(compoundTag);
}
}
@@ -630,7 +612,7 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
if (!this.offhand.get(i).isEmpty()) {
CompoundTag compoundTag = new CompoundTag();
compoundTag.putByte("Slot", (byte)(i + 150));
this.offhand.get(i).save(this.player.registryAccess(), compoundTag);
this.offhand.get(i).save(compoundTag);
listTag.add(compoundTag);
}
}
@@ -647,13 +629,15 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
for(int i = 0; i < listTag.size(); ++i) {
CompoundTag compoundTag = listTag.getCompound(i);
int j = compoundTag.getByte("Slot") & 255;
ItemStack itemstack = ItemStack.parse(this.player.registryAccess(), compoundTag).orElse(ItemStack.EMPTY);
if (j < this.items.size()) {
this.items.set(j, itemstack);
} else if (j >= 100 && j < this.armor.size() + 100) {
this.armor.set(j - 100, itemstack);
} else if (j >= 150 && j < this.offhand.size() + 150) {
this.offhand.set(j - 150, itemstack);
ItemStack itemstack = ItemStack.of(compoundTag);
if (!itemstack.isEmpty()) {
if (j < this.items.size()) {
this.items.set(j, itemstack);
} else if (j >= 100 && j < this.armor.size() + 100) {
this.armor.set(j - 100, itemstack);
} else if (j >= 150 && j < this.offhand.size() + 150) {
this.offhand.set(j - 150, itemstack);
}
}
}
@@ -690,6 +674,23 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
return this.armor.get(index);
}
@Override
public void hurtArmor(DamageSource damagesource, float damage, int[] armorIndices) {
if (damage > 0.0F) {
damage /= 4.0F;
if (damage < 1.0F) {
damage = 1.0F;
}
for (int index : armorIndices) {
ItemStack itemstack = this.armor.get(index);
if ((!damagesource.isFire() || !itemstack.getItem().isFireResistant()) && itemstack.getItem() instanceof ArmorItem) {
itemstack.hurtAndBreak((int) damage, this.player, localPlayer -> localPlayer.broadcastBreakEvent(EquipmentSlot.byTypeAndIndex(EquipmentSlot.Type.ARMOR, index)));
}
}
}
}
@Override
public void dropAll() {
for (NonNullList<ItemStack> compartment : this.compartments) {
@@ -720,12 +721,12 @@ public class SpecialPlayerInventory extends Inventory implements ISpecialPlayerI
@Override
public boolean contains(ItemStack itemstack) {
return contains(itemStack -> !itemStack.isEmpty() && ItemStack.isSameItemSameComponents(itemstack, itemStack));
return contains(itemStack -> itemStack.isEmpty() && itemStack.sameItem(itemstack));
}
@Override
public boolean contains(TagKey<Item> tagKey) {
return contains(itemStack -> !itemStack.isEmpty() && itemStack.is(tagKey));
public boolean contains(Tag<Item> tag) {
return contains(itemStack -> !itemStack.isEmpty() && itemStack.is(tag));
}
@Override
@@ -1,208 +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.internal.v1_20_R3;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.util.ReflectionHelper;
import com.lishid.openinv.util.lang.LanguageManager;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.ServerPlayerGameMode;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.SimpleMenuProvider;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.PlayerEnderChestContainer;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.BarrelBlock;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.ChestBlock;
import net.minecraft.world.level.block.ShulkerBoxBlock;
import net.minecraft.world.level.block.TrappedChestBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.bukkit.GameMode;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.logging.Logger;
public class AnySilentContainer implements IAnySilentContainer {
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
private @Nullable Field serverPlayerGameModeGameType;
public AnySilentContainer(@NotNull Logger logger, @NotNull LanguageManager lang) {
this.logger = logger;
this.lang = lang;
try {
try {
this.serverPlayerGameModeGameType = ServerPlayerGameMode.class.getDeclaredField("b");
this.serverPlayerGameModeGameType.setAccessible(true);
} catch (NoSuchFieldException e) {
logger.warning("ServerPlayerGameMode#gameModeForPlayer's obfuscated name has changed!");
logger.warning("Please report this at https://github.com/Jikoo/OpenInv/issues");
logger.warning("Attempting to fall through using reflection. Please verify that SilentContainer does not fail.");
// N.B. gameModeForPlayer is (for now) declared before previousGameModeForPlayer so silent shouldn't break.
this.serverPlayerGameModeGameType = ReflectionHelper.grabFieldByType(ServerPlayerGameMode.class, GameType.class);
}
} catch (SecurityException e) {
logger.warning("Unable to directly write player game mode! SilentContainer will fail.");
logger.log(java.util.logging.Level.WARNING, "Error obtaining GameType field", e);
}
}
@Override
public boolean activateContainer(
@NotNull final Player bukkitPlayer,
final boolean silentchest,
@NotNull final org.bukkit.block.Block bukkitBlock) {
// Silent ender chest is API-only
if (silentchest && bukkitBlock.getType() == Material.ENDER_CHEST) {
bukkitPlayer.openInventory(bukkitPlayer.getEnderChest());
bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
return true;
}
ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
final net.minecraft.world.level.Level level = player.level();
final BlockPos blockPos = new BlockPos(bukkitBlock.getX(), bukkitBlock.getY(), bukkitBlock.getZ());
final BlockEntity blockEntity = level.getBlockEntity(blockPos);
if (blockEntity == null) {
return false;
}
if (blockEntity instanceof EnderChestBlockEntity enderChestTile) {
// Anychest ender chest. See net.minecraft.world.level.block.EnderChestBlock
PlayerEnderChestContainer enderChest = player.getEnderChestInventory();
enderChest.setActiveChest(enderChestTile);
player.openMenu(new SimpleMenuProvider((containerCounter, playerInventory, ignored) -> {
MenuType<?> containers = PlayerManager.getContainers(enderChest.getContainerSize());
int rows = enderChest.getContainerSize() / 9;
return new ChestMenu(containers, containerCounter, playerInventory, enderChest, rows);
}, Component.translatable("container.enderchest")));
bukkitPlayer.incrementStatistic(Statistic.ENDERCHEST_OPENED);
return true;
}
if (!(blockEntity instanceof MenuProvider menuProvider)) {
return false;
}
BlockState blockState = level.getBlockState(blockPos);
Block block = blockState.getBlock();
if (block instanceof ChestBlock chestBlock) {
// boolean flag: do not check if chest is blocked
menuProvider = chestBlock.getMenuProvider(blockState, level, blockPos, true);
if (menuProvider == null) {
lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
return false;
}
if (block instanceof TrappedChestBlock) {
bukkitPlayer.incrementStatistic(Statistic.TRAPPED_CHEST_TRIGGERED);
} else {
bukkitPlayer.incrementStatistic(Statistic.CHEST_OPENED);
}
}
if (block instanceof ShulkerBoxBlock) {
bukkitPlayer.incrementStatistic(Statistic.SHULKER_BOX_OPENED);
}
if (block instanceof BarrelBlock) {
bukkitPlayer.incrementStatistic(Statistic.OPEN_BARREL);
}
// AnyChest only - SilentChest not active, container unsupported, or unnecessary.
if (!silentchest || player.gameMode.getGameModeForPlayer() == GameType.SPECTATOR) {
player.openMenu(menuProvider);
return true;
}
// SilentChest requires access to setting players' game mode directly.
if (this.serverPlayerGameModeGameType == null) {
return false;
}
if (blockEntity instanceof RandomizableContainerBlockEntity lootable) {
if (lootable.lootTable != null) {
lang.sendSystemMessage(bukkitPlayer, "messages.error.lootNotGenerated");
return false;
}
}
GameType gameType = player.gameMode.getGameModeForPlayer();
this.forceGameType(player, GameType.SPECTATOR);
player.openMenu(menuProvider);
this.forceGameType(player, gameType);
return true;
}
@Override
public void deactivateContainer(@NotNull final Player bukkitPlayer) {
if (this.serverPlayerGameModeGameType == null || bukkitPlayer.getGameMode() == GameMode.SPECTATOR) {
return;
}
ServerPlayer player = PlayerManager.getHandle(bukkitPlayer);
// Force game mode change without informing plugins or players.
// Regular game mode set calls GameModeChangeEvent and is cancellable.
GameType gameType = player.gameMode.getGameModeForPlayer();
this.forceGameType(player, GameType.SPECTATOR);
// ServerPlayer#closeContainer cannot be called without entering an
// infinite loop because this method is called during inventory close.
// From ServerPlayer#closeContainer -> CraftEventFactory#handleInventoryCloseEvent
player.containerMenu.transferTo(player.inventoryMenu, player.getBukkitEntity());
// From ServerPlayer#closeContainer
player.doCloseContainer();
// Regular inventory close will handle the rest - packet sending, etc.
// Revert forced game mode.
this.forceGameType(player, gameType);
}
private void forceGameType(final ServerPlayer player, final GameType gameMode) {
if (this.serverPlayerGameModeGameType == null) {
// No need to warn repeatedly, error on startup and lack of function should be enough.
return;
}
try {
this.serverPlayerGameModeGameType.setAccessible(true);
this.serverPlayerGameModeGameType.set(player.gameMode, gameMode);
} catch (IllegalArgumentException | IllegalAccessException e) {
logger.log(java.util.logging.Level.WARNING, "Error bypassing GameModeChangeEvent", e);
}
}
}
@@ -1,64 +0,0 @@
package com.lishid.openinv.internal.v1_20_R3;
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.util.lang.LanguageManager;
import net.minecraft.world.Container;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftInventory;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Logger;
public class InternalAccessor implements Accessor {
private final @NotNull PlayerManager manager;
private final @NotNull AnySilentContainer anySilentContainer;
public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
manager = new PlayerManager(logger, lang);
anySilentContainer = new AnySilentContainer(logger, lang);
}
@Override
public @NotNull PlayerManager getPlayerManager() {
return manager;
}
@Override
public @NotNull IAnySilentContainer getAnySilentContainer() {
return anySilentContainer;
}
@Override
public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
return new SpecialPlayerInventory(player, player.isOnline());
}
@Override
public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) {
return new SpecialEnderChest(player, player.isOnline());
}
@Override
public <T extends ISpecialInventory> @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class<T> clazz) {
if (!(bukkitInventory instanceof CraftInventory craftInventory)) {
return null;
}
Container container = craftInventory.getInventory();
if (clazz.isInstance(container)) {
return clazz.cast(container);
}
return null;
}
@Override
public void reload(@NotNull ConfigurationSection config) {}
}
@@ -1,184 +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.internal.v1_20_R3;
import com.lishid.openinv.event.OpenEvents;
import com.mojang.logging.LogUtils;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NumericTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.storage.PlayerDataStorage;
import org.bukkit.craftbukkit.v1_20_R3.CraftServer;
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
public class OpenPlayer extends CraftPlayer {
private static final Set<String> RESET_TAGS = Set.of(
// net.minecraft.world.Entity#saveWithoutId(CompoundTag)
"CustomName",
"CustomNameVisible",
"Silent",
"NoGravity",
"Glowing",
"TicksFrozen",
"HasVisualFire",
"Tags",
"Passengers",
// net.minecraft.server.level.ServerPlayer#addAdditionalSaveData(CompoundTag)
// Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle
"warden_spawn_tracker",
"enteredNetherPosition",
"SpawnX",
"SpawnY",
"SpawnZ",
"SpawnForced",
"SpawnAngle",
"SpawnDimension",
// net.minecraft.world.entity.player.Player#addAdditionalSaveData(CompoundTag)
"ShoulderEntityLeft",
"ShoulderEntityRight",
"LastDeathLocation",
// net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(CompoundTag)
"ActiveEffects", // Backwards compat: Renamed from 1.19
"active_effects",
"SleepingX",
"SleepingY",
"SleepingZ",
"Brain"
);
private final PlayerManager manager;
OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) {
super(server, entity);
this.manager = manager;
}
@Override
public void loadData() {
manager.loadData(getHandle());
}
@Override
public void saveData() {
if (OpenEvents.saveCancelled(this)) {
return;
}
ServerPlayer player = this.getHandle();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try {
PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
CompoundTag oldData = isOnline() ? null : worldNBTStorage.getPlayerData(player.getStringUUID());
CompoundTag playerData = getWritableTag(oldData);
playerData = player.saveWithoutId(playerData);
setExtraData(playerData);
if (oldData != null) {
// Revert certain special data values when offline.
revertSpecialValues(playerData, oldData);
}
Path playerDataDir = worldNBTStorage.getPlayerDir().toPath();
Path file = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
NbtIo.writeCompressed(playerData, file);
Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
Util.safeReplaceFile(dataFile, file, backupFile);
} catch (Exception e) {
LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
}
}
@Contract("null -> new")
private @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
if (oldData == null) {
return new CompoundTag();
}
// Copy old data. This is a deep clone, so operating on it should be safe.
oldData = oldData.copy();
// Remove vanilla/server data that is not written every time.
oldData.getAllKeys()
.removeIf(key -> RESET_TAGS.contains(key) || key.startsWith("Bukkit"));
return oldData;
}
private void revertSpecialValues(@NotNull CompoundTag newData, @NotNull CompoundTag oldData) {
// Revert automatic updates to play timestamps.
copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class);
}
private <T extends Tag> void copyValue(
@NotNull CompoundTag source,
@NotNull CompoundTag target,
@NotNull String container,
@NotNull String key,
@NotNull Class<T> tagType) {
CompoundTag oldContainer = getTag(source, container, CompoundTag.class);
CompoundTag newContainer = getTag(target, container, CompoundTag.class);
// New container being null means the server implementation doesn't store this data.
if (newContainer == null) {
return;
}
// If old tag exists, copy it to new location, removing otherwise.
setTag(newContainer, key, getTag(oldContainer, key, tagType));
}
private <T extends Tag> @Nullable T getTag(
@Nullable CompoundTag container,
@NotNull String key,
@NotNull Class<T> dataType) {
if (container == null) {
return null;
}
Tag value = container.get(key);
if (value == null || !dataType.isAssignableFrom(value.getClass())) {
return null;
}
return dataType.cast(value);
}
private <T extends Tag> void setTag(
@NotNull CompoundTag container,
@NotNull String key,
@Nullable T data) {
if (data == null) {
container.remove(key);
} else {
container.put(key, data);
}
}
}
@@ -1,297 +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.internal.v1_20_R3;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.InventoryViewTitle;
import com.lishid.openinv.internal.OpenInventoryView;
import com.lishid.openinv.util.lang.LanguageManager;
import com.mojang.authlib.GameProfile;
import com.mojang.serialization.Dynamic;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.ChatVisiblity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R3.CraftServer;
import org.bukkit.craftbukkit.v1_20_R3.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R3.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R3.event.CraftEventFactory;
import org.bukkit.craftbukkit.v1_20_R3.inventory.CraftContainer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.logging.Logger;
public class PlayerManager implements com.lishid.openinv.internal.PlayerManager {
private static boolean paper;
static {
try {
Class.forName("io.papermc.paper.configuration.Configuration");
paper = true;
} catch (ClassNotFoundException ignored) {
paper = false;
}
}
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
private @Nullable Field bukkitEntity;
public PlayerManager(@NotNull Logger logger, @NotNull LanguageManager lang) {
this.logger = logger;
this.lang = lang;
try {
bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
} catch (NoSuchFieldException e) {
logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!");
logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
bukkitEntity = null;
}
}
public static @NotNull ServerPlayer getHandle(final Player player) {
if (player instanceof CraftPlayer) {
return ((CraftPlayer) player).getHandle();
}
Server server = player.getServer();
ServerPlayer nmsPlayer = null;
if (server instanceof CraftServer) {
nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getUniqueId());
}
if (nmsPlayer == null) {
// Could use reflection to examine fields, but it's honestly not worth the bother.
throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName());
}
return nmsPlayer;
}
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
if (worldServer == null) {
return null;
}
// Create a new ServerPlayer.
ServerPlayer entity = createNewPlayer(server, worldServer, offline);
// Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak.
entity.getAdvancements().stopListening();
// Try to load the player's data.
if (loadData(entity)) {
// If data is loaded successfully, return the Bukkit entity.
return entity.getBukkitEntity();
}
return null;
}
private @NotNull ServerPlayer createNewPlayer(
@NotNull MinecraftServer server,
@NotNull ServerLevel worldServer,
@NotNull final OfflinePlayer offline) {
// See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile)
// See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket)
GameProfile profile = new GameProfile(offline.getUniqueId(),
offline.getName() != null ? offline.getName() : offline.getUniqueId().toString());
ClientInformation dummyInfo = new ClientInformation(
"en_us",
1, // Reduce distance just in case.
ChatVisiblity.HIDDEN, // Don't accept chat.
false,
ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION,
ServerPlayer.DEFAULT_MAIN_HAND,
true,
false // Don't list in player list (not that this player is in the list anyway).
);
ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo);
try {
injectPlayer(entity);
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!");
}
return entity;
}
boolean loadData(@NotNull ServerPlayer player) {
// See CraftPlayer#loadData
CompoundTag loadedData = player.server.getPlayerList().playerIo.load(player);
if (loadedData == null) {
// Exceptions with loading are logged by Mojang.
return false;
}
// Read basic data into the player.
player.load(loadedData);
// Also read "extra" data.
player.readAdditionalSaveData(loadedData);
// Game type settings are also loaded separately.
player.loadGameTypes(loadedData);
if (paper) {
// Paper: world is not loaded by ServerPlayer#load(CompoundTag).
parseWorld(player, loadedData);
}
return true;
}
private void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) {
// See PlayerList#placeNewPlayer
World bukkitWorld;
if (loadedData.contains("WorldUUIDMost") && loadedData.contains("WorldUUIDLeast")) {
// Modern Bukkit world.
bukkitWorld = Bukkit.getServer().getWorld(new UUID(loadedData.getLong("WorldUUIDMost"), loadedData.getLong("WorldUUIDLeast")));
} else if (loadedData.contains("world", net.minecraft.nbt.Tag.TAG_STRING)) {
// Legacy Bukkit world.
bukkitWorld = Bukkit.getServer().getWorld(loadedData.getString("world"));
} else {
// Vanilla player data.
DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension")))
.resultOrPartial(logger::warning)
.map(player.server::getLevel)
// If ServerLevel exists, set, otherwise move to spawn.
.ifPresentOrElse(player::setServerLevel, () -> player.spawnIn(null));
return;
}
if (bukkitWorld == null) {
player.spawnIn(null);
return;
}
player.setServerLevel(((CraftWorld) bukkitWorld).getHandle());
}
private void injectPlayer(ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this));
}
@Override
public @NotNull Player inject(@NotNull Player player) {
try {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) {
return openPlayer;
}
injectPlayer(nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!");
return player;
}
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.connection == null) {
return null;
}
InventoryViewTitle title = InventoryViewTitle.of(inventory);
if (title == null) {
return player.openInventory(inventory.getBukkitInventory());
}
InventoryView view = new OpenInventoryView(player, inventory, title.getTitle(lang, player, inventory));
AbstractContainerMenu container = new CraftContainer(view, nmsPlayer, nmsPlayer.nextContainerCounter()) {
@Override
public MenuType<?> getType() {
return getContainers(inventory.getBukkitInventory().getSize());
}
};
container.setTitle(Component.literal(view.getOriginalTitle()));
container = CraftEventFactory.callInventoryOpenEvent(nmsPlayer, container);
if (container == null) {
return null;
}
nmsPlayer.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(),
Component.literal(container.getBukkitView().getTitle())));
nmsPlayer.containerMenu = container;
nmsPlayer.initMenu(container);
return container.getBukkitView();
}
static @NotNull MenuType<?> getContainers(int inventorySize) {
return switch (inventorySize) {
case 9 -> MenuType.GENERIC_9x1;
case 18 -> MenuType.GENERIC_9x2;
case 36 -> MenuType.GENERIC_9x4; // PLAYER
case 41, 45 -> MenuType.GENERIC_9x5;
case 54 -> MenuType.GENERIC_9x6;
default -> MenuType.GENERIC_9x3; // Default 27-slot inventory
};
}
}
@@ -1,64 +0,0 @@
package com.lishid.openinv.internal.v1_20_R4;
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.util.lang.LanguageManager;
import net.minecraft.world.Container;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftInventory;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Logger;
public class InternalAccessor implements Accessor {
private final @NotNull PlayerManager manager;
private final @NotNull AnySilentContainer anySilentContainer;
public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
manager = new PlayerManager(logger, lang);
anySilentContainer = new AnySilentContainer(logger, lang);
}
@Override
public @NotNull PlayerManager getPlayerManager() {
return manager;
}
@Override
public @NotNull IAnySilentContainer getAnySilentContainer() {
return anySilentContainer;
}
@Override
public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
return new SpecialPlayerInventory(player, player.isOnline());
}
@Override
public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) {
return new SpecialEnderChest(player, player.isOnline());
}
@Override
public <T extends ISpecialInventory> @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class<T> clazz) {
if (!(bukkitInventory instanceof CraftInventory craftInventory)) {
return null;
}
Container container = craftInventory.getInventory();
if (clazz.isInstance(container)) {
return clazz.cast(container);
}
return null;
}
@Override
public void reload(@NotNull ConfigurationSection config) {}
}
@@ -1,194 +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.internal.v1_20_R4;
import com.lishid.openinv.event.OpenEvents;
import com.mojang.logging.LogUtils;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NumericTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.storage.PlayerDataStorage;
import org.bukkit.craftbukkit.v1_20_R4.CraftServer;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
public class OpenPlayer extends CraftPlayer {
/**
* List of tags to always reset when saving.
*
* @see net.minecraft.world.entity.Entity#saveWithoutId(CompoundTag)
* @see net.minecraft.server.level.ServerPlayer#addAdditionalSaveData(CompoundTag)
* @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(CompoundTag)
* @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(CompoundTag)
*/
private static final Set<String> RESET_TAGS = Set.of(
// Entity#saveWithoutId(CompoundTag)
"CustomName",
"CustomNameVisible",
"Silent",
"NoGravity",
"Glowing",
"TicksFrozen",
"HasVisualFire",
"Tags",
"Passengers",
// ServerPlayer#addAdditionalSaveData(CompoundTag)
// Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle
"warden_spawn_tracker",
"enteredNetherPosition",
"SpawnX",
"SpawnY",
"SpawnZ",
"SpawnForced",
"SpawnAngle",
"SpawnDimension",
"raid_omen_position",
// Player#addAdditionalSaveData(CompoundTag)
"ShoulderEntityLeft",
"ShoulderEntityRight",
"LastDeathLocation",
"current_explosion_impact_pos",
// LivingEntity#addAdditionalSaveData(CompoundTag)
"ActiveEffects", // Backwards compat: Renamed from 1.19
"active_effects",
"SleepingX",
"SleepingY",
"SleepingZ",
"Brain"
);
private final PlayerManager manager;
OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) {
super(server, entity);
this.manager = manager;
}
@Override
public void loadData() {
manager.loadData(getHandle());
}
@Override
public void saveData() {
if (OpenEvents.saveCancelled(this)) {
return;
}
ServerPlayer player = this.getHandle();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try {
PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
CompoundTag oldData = isOnline() ? null : worldNBTStorage.load(player.getName().getString(), player.getStringUUID()).orElse(null);
CompoundTag playerData = getWritableTag(oldData);
playerData = player.saveWithoutId(playerData);
setExtraData(playerData);
if (oldData != null) {
// Revert certain special data values when offline.
revertSpecialValues(playerData, oldData);
}
Path playerDataDir = worldNBTStorage.getPlayerDir().toPath();
Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
NbtIo.writeCompressed(playerData, tempFile);
Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
Util.safeReplaceFile(dataFile, tempFile, backupFile);
} catch (Exception e) {
LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
}
}
@Contract("null -> new")
private @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
if (oldData == null) {
return new CompoundTag();
}
// Copy old data. This is a deep clone, so operating on it should be safe.
oldData = oldData.copy();
// Remove vanilla/server data that is not written every time.
oldData.getAllKeys()
.removeIf(key -> RESET_TAGS.contains(key) || key.startsWith("Bukkit"));
return oldData;
}
private void revertSpecialValues(@NotNull CompoundTag newData, @NotNull CompoundTag oldData) {
// Revert automatic updates to play timestamps.
copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class);
}
private <T extends Tag> void copyValue(
@NotNull CompoundTag source,
@NotNull CompoundTag target,
@NotNull String container,
@NotNull String key,
@NotNull Class<T> tagType) {
CompoundTag oldContainer = getTag(source, container, CompoundTag.class);
CompoundTag newContainer = getTag(target, container, CompoundTag.class);
// New container being null means the server implementation doesn't store this data.
if (newContainer == null) {
return;
}
// If old tag exists, copy it to new location, removing otherwise.
setTag(newContainer, key, getTag(oldContainer, key, tagType));
}
private <T extends Tag> @Nullable T getTag(
@Nullable CompoundTag container,
@NotNull String key,
@NotNull Class<T> dataType) {
if (container == null) {
return null;
}
Tag value = container.get(key);
if (value == null || !dataType.isAssignableFrom(value.getClass())) {
return null;
}
return dataType.cast(value);
}
private <T extends Tag> void setTag(
@NotNull CompoundTag container,
@NotNull String key,
@Nullable T data) {
if (data == null) {
container.remove(key);
} else {
container.put(key, data);
}
}
}
@@ -1,297 +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.internal.v1_20_R4;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.InventoryViewTitle;
import com.lishid.openinv.internal.OpenInventoryView;
import com.lishid.openinv.util.lang.LanguageManager;
import com.mojang.authlib.GameProfile;
import com.mojang.serialization.Dynamic;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.ChatVisiblity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_20_R4.CraftServer;
import org.bukkit.craftbukkit.v1_20_R4.CraftWorld;
import org.bukkit.craftbukkit.v1_20_R4.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_20_R4.event.CraftEventFactory;
import org.bukkit.craftbukkit.v1_20_R4.inventory.CraftContainer;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.logging.Logger;
public class PlayerManager implements com.lishid.openinv.internal.PlayerManager {
private static boolean paper;
static {
try {
Class.forName("io.papermc.paper.configuration.Configuration");
paper = true;
} catch (ClassNotFoundException ignored) {
paper = false;
}
}
private final @NotNull Logger logger;
private final @NotNull LanguageManager lang;
private @Nullable Field bukkitEntity;
public PlayerManager(@NotNull Logger logger, @NotNull LanguageManager lang) {
this.logger = logger;
this.lang = lang;
try {
bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
} catch (NoSuchFieldException e) {
logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!");
logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
bukkitEntity = null;
}
}
public static @NotNull ServerPlayer getHandle(final Player player) {
if (player instanceof CraftPlayer) {
return ((CraftPlayer) player).getHandle();
}
Server server = player.getServer();
ServerPlayer nmsPlayer = null;
if (server instanceof CraftServer) {
nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getUniqueId());
}
if (nmsPlayer == null) {
// Could use reflection to examine fields, but it's honestly not worth the bother.
throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName());
}
return nmsPlayer;
}
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
if (worldServer == null) {
return null;
}
// Create a new ServerPlayer.
ServerPlayer entity = createNewPlayer(server, worldServer, offline);
// Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak.
entity.getAdvancements().stopListening();
// Try to load the player's data.
if (loadData(entity)) {
// If data is loaded successfully, return the Bukkit entity.
return entity.getBukkitEntity();
}
return null;
}
private @NotNull ServerPlayer createNewPlayer(
@NotNull MinecraftServer server,
@NotNull ServerLevel worldServer,
@NotNull final OfflinePlayer offline) {
// See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile)
// See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket)
GameProfile profile = new GameProfile(offline.getUniqueId(),
offline.getName() != null ? offline.getName() : offline.getUniqueId().toString());
ClientInformation dummyInfo = new ClientInformation(
"en_us",
1, // Reduce distance just in case.
ChatVisiblity.HIDDEN, // Don't accept chat.
false,
ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION,
ServerPlayer.DEFAULT_MAIN_HAND,
true,
false // Don't list in player list (not that this player is in the list anyway).
);
ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo);
try {
injectPlayer(entity);
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!");
}
return entity;
}
boolean loadData(@NotNull ServerPlayer player) {
// See CraftPlayer#loadData
CompoundTag loadedData = player.server.getPlayerList().playerIo.load(player).orElse(null);
if (loadedData == null) {
// Exceptions with loading are logged by Mojang.
return false;
}
// Read basic data into the player.
player.load(loadedData);
// Also read "extra" data.
player.readAdditionalSaveData(loadedData);
// Game type settings are also loaded separately.
player.loadGameTypes(loadedData);
if (paper) {
// Paper: world is not loaded by ServerPlayer#load(CompoundTag).
parseWorld(player, loadedData);
}
return true;
}
private void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) {
// See PlayerList#placeNewPlayer
World bukkitWorld;
if (loadedData.contains("WorldUUIDMost") && loadedData.contains("WorldUUIDLeast")) {
// Modern Bukkit world.
bukkitWorld = Bukkit.getServer().getWorld(new UUID(loadedData.getLong("WorldUUIDMost"), loadedData.getLong("WorldUUIDLeast")));
} else if (loadedData.contains("world", net.minecraft.nbt.Tag.TAG_STRING)) {
// Legacy Bukkit world.
bukkitWorld = Bukkit.getServer().getWorld(loadedData.getString("world"));
} else {
// Vanilla player data.
DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension")))
.resultOrPartial(logger::warning)
.map(player.server::getLevel)
// If ServerLevel exists, set, otherwise move to spawn.
.ifPresentOrElse(player::setServerLevel, () -> player.spawnIn(null));
return;
}
if (bukkitWorld == null) {
player.spawnIn(null);
return;
}
player.setServerLevel(((CraftWorld) bukkitWorld).getHandle());
}
private void injectPlayer(ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this));
}
@Override
public @NotNull Player inject(@NotNull Player player) {
try {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) {
return openPlayer;
}
injectPlayer(nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!");
return player;
}
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.connection == null) {
return null;
}
InventoryViewTitle title = InventoryViewTitle.of(inventory);
if (title == null) {
return player.openInventory(inventory.getBukkitInventory());
}
InventoryView view = new OpenInventoryView(player, inventory, title.getTitle(lang, player, inventory));
AbstractContainerMenu container = new CraftContainer(view, nmsPlayer, nmsPlayer.nextContainerCounter()) {
@Override
public MenuType<?> getType() {
return getContainers(inventory.getBukkitInventory().getSize());
}
};
container.setTitle(Component.literal(view.getOriginalTitle()));
container = CraftEventFactory.callInventoryOpenEvent(nmsPlayer, container);
if (container == null) {
return null;
}
nmsPlayer.connection.send(new ClientboundOpenScreenPacket(container.containerId, container.getType(),
Component.literal(container.getBukkitView().getTitle())));
nmsPlayer.containerMenu = container;
nmsPlayer.initMenu(container);
return container.getBukkitView();
}
static @NotNull MenuType<?> getContainers(int inventorySize) {
return switch (inventorySize) {
case 9 -> MenuType.GENERIC_9x1;
case 18 -> MenuType.GENERIC_9x2;
case 36 -> MenuType.GENERIC_9x4; // PLAYER
case 41, 45 -> MenuType.GENERIC_9x5;
case 54 -> MenuType.GENERIC_9x6;
default -> MenuType.GENERIC_9x3; // Default 27-slot inventory
};
}
}
-77
View File
@@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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/>.
-->
<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.0.0</version>
</parent>
<artifactId>openinvadapter1_21_R1</artifactId>
<name>OpenInvAdapter1_21_R1</name>
<properties>
<maven.compiler.system>21</maven.compiler.system>
<maven.compiler.release>21</maven.compiler.release>
<spigot.version>1.21-R0.1-SNAPSHOT</spigot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>${spigot.version}</version>
</dependency>
<dependency>
<artifactId>spigot</artifactId>
<groupId>org.spigotmc</groupId>
<scope>provided</scope>
<version>${spigot.version}</version>
<classifier>remapped-mojang</classifier>
</dependency>
<dependency>
<artifactId>openinvapi</artifactId>
<groupId>com.lishid</groupId>
</dependency>
<dependency>
<artifactId>openinvcommon</artifactId>
<groupId>com.lishid</groupId>
</dependency>
<dependency>
<artifactId>annotations</artifactId>
<groupId>org.jetbrains</groupId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>net.md-5</groupId>
<artifactId>specialsource-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -1,81 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1;
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.v1_21_R1.container.AnySilentContainer;
import com.lishid.openinv.internal.v1_21_R1.container.OpenEnderChest;
import com.lishid.openinv.internal.v1_21_R1.container.OpenInventory;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import com.lishid.openinv.internal.v1_21_R1.player.PlayerManager;
import com.lishid.openinv.util.lang.LanguageManager;
import net.minecraft.world.Container;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventory;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Level;
import java.util.logging.Logger;
public class InternalAccessor implements Accessor {
private final @NotNull Logger logger;
private final @NotNull PlayerManager manager;
private final @NotNull AnySilentContainer anySilentContainer;
public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
this.logger = logger;
manager = new PlayerManager(logger);
anySilentContainer = new AnySilentContainer(logger, lang);
}
@Override
public @NotNull PlayerManager getPlayerManager() {
return manager;
}
@Override
public @NotNull IAnySilentContainer getAnySilentContainer() {
return anySilentContainer;
}
@Override
public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
return new OpenInventory(player);
}
@Override
public @NotNull ISpecialEnderChest createEnderChest(@NotNull Player player) {
return new OpenEnderChest(player);
}
@Override
public <T extends ISpecialInventory> @Nullable T get(@NotNull Inventory bukkitInventory, @NotNull Class<T> clazz) {
if (!(bukkitInventory instanceof CraftInventory craftInventory)) {
return null;
}
Container container = craftInventory.getInventory();
if (clazz.isInstance(container)) {
return clazz.cast(container);
}
return null;
}
@Override
public void reload(@NotNull ConfigurationSection config) {
ConfigurationSection placeholders = config.getConfigurationSection("placeholders");
if (placeholders != null) {
try {
Placeholders.load(placeholders);
} catch (Exception e) {
logger.log(Level.WARNING, "Caught exception loading placeholder overrides!", e);
}
}
}
}
@@ -1,307 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.google.common.base.Suppliers;
import com.lishid.openinv.internal.v1_21_R1.container.slot.SlotPlaceholder;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.inventory.ContainerSynchronizer;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* An extension of {@link AbstractContainerMenu} that supports {@link SlotPlaceholder placeholders}.
*/
public abstract class OpenContainerMenu extends AbstractContainerMenu {
private @Nullable ContainerSynchronizer synchronizer;
private final List<DataSlot> dataSlots = new ArrayList<>();
private final IntList remoteDataSlots = new IntArrayList();
private final List<ContainerListener> containerListeners = new ArrayList<>();
private ItemStack remoteCarried = ItemStack.EMPTY;
private boolean suppressRemoteUpdates;
protected OpenContainerMenu(@Nullable MenuType<?> containers, int containerCounter) {
super(containers, containerCounter);
}
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;
};
}
/**
* Reimplementation of {@link AbstractContainerMenu#moveItemStackTo(ItemStack, int, int, boolean)} that ignores fake
* slots and respects {@link Slot#hasItem()}.
*
* @param itemStack the stack to quick-move
* @param rangeLow the start of the range of slots that can be moved to, inclusive
* @param rangeHigh the end of the range of slots that can be moved to, exclusive
* @param topDown whether to start at the top of the range or bottom
* @return whether the stack was modified as a result of being quick-moved
*/
@Override
protected boolean moveItemStackTo(ItemStack itemStack, int rangeLow, int rangeHigh, boolean topDown) {
boolean modified = false;
boolean stackable = itemStack.isStackable();
Slot firstEmpty = null;
for (int index = topDown ? rangeHigh - 1 : rangeLow;
!itemStack.isEmpty() && (topDown ? index >= rangeLow : index < rangeHigh);
index += topDown ? -1 : 1
) {
Slot slot = slots.get(index);
// If the slot cannot be added to, check the next slot.
if (slot.isFake() || !slot.mayPlace(itemStack)) {
continue;
}
if (slot.hasItem()) {
// If the item isn't stackable, check the next slot.
if (!stackable) {
continue;
}
// Otherwise, add as many as we can from our stack to the slot.
modified = addToExistingStack(itemStack, slot);
} else {
// If this is the first empty slot, keep track of it for later use.
if (firstEmpty == null) {
firstEmpty = slot;
}
// If the item isn't stackable, we've located the slot we're adding it to, so we're done.
if (!stackable) {
break;
}
}
}
// If the item hasn't been fully added yet, add as many as we can to the first open slot.
if (!itemStack.isEmpty() && firstEmpty != null) {
firstEmpty.setByPlayer(itemStack.split(Math.min(itemStack.getCount(), firstEmpty.getMaxStackSize(itemStack))));
firstEmpty.setChanged();
modified = true;
}
return modified;
}
private static boolean addToExistingStack(ItemStack itemStack, Slot slot) {
ItemStack existing = slot.getItem();
// If the items aren't the same, we can't add our item.
if (!ItemStack.isSameItemSameComponents(itemStack, existing)) {
return false;
}
int total = existing.getCount() + itemStack.getCount();
int max = slot.getMaxStackSize(existing);
// If the existing item can accept the entirety of our item, we're done!
if (total <= max) {
itemStack.setCount(0);
existing.setCount(total);
slot.setChanged();
return true;
}
// Otherwise, add as many as we can.
itemStack.shrink(max - existing.getCount());
existing.setCount(max);
slot.setChanged();
return true;
}
// Overrides from here on are purely to modify the sync process to send placeholder items.
@Override
protected Slot addSlot(Slot slot) {
slot.index = this.slots.size();
this.slots.add(slot);
this.lastSlots.add(ItemStack.EMPTY);
this.remoteSlots.add(ItemStack.EMPTY);
return slot;
}
@Override
protected DataSlot addDataSlot(DataSlot dataSlot) {
this.dataSlots.add(dataSlot);
this.remoteDataSlots.add(0);
return dataSlot;
}
@Override
protected void addDataSlots(ContainerData containerData) {
for (int i = 0; i < containerData.getCount(); i++) {
this.addDataSlot(DataSlot.forContainer(containerData, i));
}
}
@Override
public void addSlotListener(ContainerListener containerListener) {
if (!this.containerListeners.contains(containerListener)) {
this.containerListeners.add(containerListener);
this.broadcastChanges();
}
}
@Override
public void setSynchronizer(ContainerSynchronizer containerSynchronizer) {
this.synchronizer = containerSynchronizer;
this.sendAllDataToRemote();
}
@Override
public void sendAllDataToRemote() {
for (int index = 0; index < slots.size(); ++index) {
Slot slot = slots.get(index);
this.remoteSlots.set(index, (slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem()).copy());
}
remoteCarried = getCarried().copy();
for (int index = 0; index < this.dataSlots.size(); ++index) {
this.remoteDataSlots.set(index, this.dataSlots.get(index).get());
}
if (this.synchronizer != null) {
this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray());
}
}
@Override
public void broadcastCarriedItem() {
this.remoteCarried = this.getCarried().copy();
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, this.remoteCarried);
}
}
@Override
public void removeSlotListener(ContainerListener containerListener) {
this.containerListeners.remove(containerListener);
}
@Override
public void broadcastChanges() {
for (int index = 0; index < this.slots.size(); ++index) {
Slot slot = this.slots.get(index);
ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
Supplier<ItemStack> supplier = Suppliers.memoize(itemstack::copy);
this.triggerSlotListeners(index, itemstack, supplier);
this.synchronizeSlotToRemote(index, itemstack, supplier);
}
this.synchronizeCarriedToRemote();
for (int index = 0; index < this.dataSlots.size(); ++index) {
DataSlot dataSlot = this.dataSlots.get(index);
int j = dataSlot.get();
if (dataSlot.checkAndClearUpdateFlag()) {
this.updateDataSlotListeners(index, j);
}
this.synchronizeDataSlotToRemote(index, j);
}
}
@Override
public void broadcastFullState() {
for (int index = 0; index < this.slots.size(); ++index) {
ItemStack itemstack = this.slots.get(index).getItem();
this.triggerSlotListeners(index, itemstack, itemstack::copy);
}
for (int index = 0; index < this.dataSlots.size(); ++index) {
DataSlot containerproperty = this.dataSlots.get(index);
if (containerproperty.checkAndClearUpdateFlag()) {
this.updateDataSlotListeners(index, containerproperty.get());
}
}
this.sendAllDataToRemote();
}
private void updateDataSlotListeners(int i, int j) {
for (ContainerListener containerListener : this.containerListeners) {
containerListener.dataChanged(this, i, j);
}
}
private void triggerSlotListeners(int index, ItemStack itemStack, Supplier<ItemStack> supplier) {
ItemStack itemStack1 = this.lastSlots.get(index);
if (!ItemStack.matches(itemStack1, itemStack)) {
ItemStack itemStack2 = supplier.get();
this.lastSlots.set(index, itemStack2);
for (ContainerListener containerListener : this.containerListeners) {
containerListener.slotChanged(this, index, itemStack2);
}
}
}
private void synchronizeSlotToRemote(int i, ItemStack itemStack, Supplier<ItemStack> supplier) {
if (!this.suppressRemoteUpdates) {
ItemStack itemStack1 = this.remoteSlots.get(i);
if (!ItemStack.matches(itemStack1, itemStack)) {
ItemStack itemstack2 = supplier.get();
this.remoteSlots.set(i, itemstack2);
if (this.synchronizer != null) {
this.synchronizer.sendSlotChange(this, i, itemstack2);
}
}
}
}
private void synchronizeDataSlotToRemote(int index, int value) {
if (!this.suppressRemoteUpdates) {
int existing = this.remoteDataSlots.getInt(index);
if (existing != value) {
this.remoteDataSlots.set(index, value);
if (this.synchronizer != null) {
this.synchronizer.sendDataChange(this, index, value);
}
}
}
}
private void synchronizeCarriedToRemote() {
if (!this.suppressRemoteUpdates && !ItemStack.matches(this.getCarried(), this.remoteCarried)) {
this.remoteCarried = this.getCarried().copy();
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, this.remoteCarried);
}
}
}
@Override
public void setRemoteCarried(ItemStack itemstack) {
this.remoteCarried = itemstack.copy();
}
@Override
public void suppressRemoteUpdates() {
this.suppressRemoteUpdates = true;
}
@Override
public void resumeRemoteUpdates() {
this.suppressRemoteUpdates = false;
}
}
@@ -1,192 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.lishid.openinv.internal.ISpecialEnderChest;
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;
import net.minecraft.world.inventory.StackedContentsCompatible;
import net.minecraft.world.item.ItemStack;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class OpenEnderChest implements Container, StackedContentsCompatible, MenuProvider, ISpecialEnderChest {
private CraftInventory inventory;
private @NotNull ServerPlayer owner;
private NonNullList<ItemStack> items;
private int maxStack = 64;
private final List<HumanEntity> transaction = new ArrayList<>();
public OpenEnderChest(@NotNull org.bukkit.entity.Player player) {
this.owner = PlayerManager.getHandle(player);
this.items = owner.getEnderChestInventory().items;
}
@Override
public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() {
if (inventory == null) {
inventory = new CraftInventory(this) {
@Override
public @NotNull InventoryType getType() {
return InventoryType.ENDER_CHEST;
}
};
}
return inventory;
}
@Override
public void setPlayerOnline(@NotNull org.bukkit.entity.Player player) {
owner = PlayerManager.getHandle(player);
NonNullList<ItemStack> activeItems = owner.getEnderChestInventory().items;
// Guard against size changing. Theoretically on Purpur all row variations still have 6 rows internally.
int max = Math.min(items.size(), activeItems.size());
for (int index = 0; index < max; ++index) {
activeItems.set(index, items.get(index));
}
items = activeItems;
}
@Override
public void setPlayerOffline() {}
@Override
public @NotNull org.bukkit.entity.Player getPlayer() {
return owner.getBukkitEntity();
}
@Override
public int getContainerSize() {
return items.size();
}
@Override
public boolean isEmpty() {
return items.stream().allMatch(ItemStack::isEmpty);
}
@Override
public ItemStack getItem(int index) {
return index >= 0 && index < items.size() ? items.get(index) : ItemStack.EMPTY;
}
@Override
public ItemStack removeItem(int index, int amount) {
ItemStack itemstack = ContainerHelper.removeItem(items, index, amount);
if (!itemstack.isEmpty()) {
setChanged();
}
return itemstack;
}
@Override
public ItemStack removeItemNoUpdate(int index) {
return index >= 0 && index < items.size() ? items.set(index, ItemStack.EMPTY) : ItemStack.EMPTY;
}
@Override
public void setItem(int index, ItemStack itemStack) {
if (index >= 0 && index < items.size()) {
items.set(index, itemStack);
}
}
@Override
public int getMaxStackSize() {
return maxStack;
}
@Override
public void setChanged() {
this.owner.getEnderChestInventory().setChanged();
}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public List<ItemStack> getContents() {
return items;
}
@Override
public void onOpen(CraftHumanEntity craftHumanEntity) {
transaction.add(craftHumanEntity);
}
@Override
public void onClose(CraftHumanEntity craftHumanEntity) {
transaction.remove(craftHumanEntity);
}
@Override
public List<HumanEntity> getViewers() {
return transaction;
}
@Override
public org.bukkit.entity.Player getOwner() {
return getPlayer();
}
@Override
public void setMaxStackSize(int size) {
maxStack = size;
}
@Override
public @Nullable Location getLocation() {
return null;
}
@Override
public void clearContent() {
items.clear();
setChanged();
}
@Override
public void fillStackedContents(StackedContents stackedContents) {
for (ItemStack itemstack : items) {
stackedContents.accountStack(itemstack);
}
}
@Override
public Component getDisplayName() {
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) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenEnderChestMenu(this, serverPlayer, i);
}
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,367 +0,0 @@
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.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;
import com.lishid.openinv.internal.v1_21_R1.container.slot.ContentCursor;
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.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 net.minecraft.ChatFormatting;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
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;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.Location;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class OpenInventory implements Container, MenuProvider, ISpecialPlayerInventory {
private final List<Content> slots;
private final int size;
private ServerPlayer owner;
private int maxStackSize = 99;
private CraftInventory bukkitEntity;
public List<HumanEntity> transaction = new ArrayList<>();
public OpenInventory(@NotNull org.bukkit.entity.Player bukkitPlayer) {
owner = PlayerManager.getHandle(bukkitPlayer);
// Get total size, rounding up to nearest 9 for client compatibility.
int rawSize = owner.getInventory().getContainerSize() + owner.inventoryMenu.getCraftSlots().getContainerSize() + 1;
size = ((int) Math.ceil(rawSize / 9.0)) * 9;
slots = NonNullList.withSize(size, new ContentViewOnly(owner));
setupSlots();
}
private void setupSlots() {
// Top of inventory: Regular contents.
int nextIndex = addMainInventory();
// If inventory is expected size, we can arrange slots to be pretty.
Inventory ownerInv = owner.getInventory();
if (ownerInv.items.size() == 36
&& ownerInv.armor.size() == 4
&& ownerInv.offhand.size() == 1
&& owner.inventoryMenu.getCraftSlots().getContainerSize() == 4) {
// Armor slots: Bottom left.
addArmor(36);
// Off-hand: Below chestplate.
addOffHand(46);
// Drop slot: Bottom right.
slots.set(53, new ContentDrop(owner));
// Cursor slot: Above drop.
slots.set(44, new ContentCursor(owner));
// Crafting is displayed in the bottom right corner.
// As we're using the pretty view, this is a 3x2.
addCrafting(41, true);
return;
}
// Otherwise we'll just add elements linearly.
nextIndex = addArmor(nextIndex);
nextIndex = addOffHand(nextIndex);
nextIndex = addCrafting(nextIndex, false);
slots.set(nextIndex, new ContentCursor(owner));
// Drop slot last.
slots.set(slots.size() - 1, new ContentDrop(owner));
}
private int addMainInventory() {
int listSize = owner.getInventory().items.size();
// Hotbar slots are 0-8. We want those to appear on the bottom of the inventory like a normal player inventory,
// so everything else needs to move up a row.
int hotbarDiff = listSize - 9;
for (int localIndex = 0; localIndex < listSize; ++localIndex) {
InventoryType.SlotType type;
int invIndex;
if (localIndex < hotbarDiff) {
invIndex = localIndex + 9;
type = InventoryType.SlotType.CONTAINER;
} else {
type = InventoryType.SlotType.QUICKBAR;
invIndex = localIndex - hotbarDiff;
}
slots.set(localIndex, new ContentList(owner, invIndex, type) {
@Override
public void setHolder(@NotNull ServerPlayer holder) {
items = holder.getInventory().items;
}
});
}
return listSize;
}
private int addArmor(int startIndex) {
int listSize = owner.getInventory().armor.size();
for (int i = 0; i < listSize; ++i) {
// Armor slots go bottom to top; boots are slot 0, helmet is slot 3.
// Since we have to display horizontally due to space restrictions,
// making the left side the "top" is more user-friendly.
int armorIndex;
EquipmentSlot slot;
switch (i) {
case 3 -> {
armorIndex = 0;
slot = EquipmentSlot.FEET;
}
case 2 -> {
armorIndex = 1;
slot = EquipmentSlot.LEGS;
}
case 1 -> {
armorIndex = 2;
slot = EquipmentSlot.CHEST;
}
case 0 -> {
armorIndex = 3;
slot = EquipmentSlot.HEAD;
}
default -> {
// In the event that new armor slots are added, they can be placed at the end.
armorIndex = i;
slot = EquipmentSlot.MAINHAND;
}
}
slots.set(startIndex + i, new ContentEquipment(owner, armorIndex, slot));
}
return startIndex + listSize;
}
private int addOffHand(int startIndex) {
int listSize = owner.getInventory().offhand.size();
for (int localIndex = 0; localIndex < listSize; ++localIndex) {
slots.set(startIndex + localIndex, new ContentOffHand(owner, localIndex));
}
return startIndex + listSize;
}
private int addCrafting(int startIndex, boolean pretty) {
int listSize = owner.inventoryMenu.getCraftSlots().getContents().size();
pretty &= listSize == 4;
for (int localIndex = 0; localIndex < listSize; ++localIndex) {
// Pretty display is a 2x2 rather than linear.
// If index is in top row, grid is not 2x2, or pretty is disabled, just use current index.
// Otherwise, subtract 2 and add 9 to start in the same position on the next row.
int modIndex = startIndex + (localIndex < 2 || !pretty ? localIndex : localIndex + 7);
slots.set(modIndex, new ContentCrafting(owner, localIndex));
}
if (pretty) {
slots.set(startIndex + 2, new ContentViewOnly(owner) {
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotViewOnly(container, slot, x, y) {
@Override
public ItemStack getOrDefault() {
return Placeholders.craftingOutput;
}
};
}
});
slots.set(startIndex + 11, new ContentCraftingResult(owner));
}
return startIndex + listSize;
}
public Slot getMenuSlot(int index, int x, int y) {
return slots.get(index).asSlot(this, index, x, y);
}
public InventoryType.SlotType getSlotType(int index) {
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.
if (owner.equals(viewer)) {
component.append(
Component.translatableWithFallback("openinv.container.inventory.self", "")
.withStyle(style -> style
.withFont(ResourceLocation.parse("openinv:font/inventory"))
.withColor(ChatFormatting.WHITE)));
} else {
component.append(
Component.translatableWithFallback("openinv.container.inventory.other", "")
.withStyle(style -> style
.withFont(ResourceLocation.parse("openinv:font/inventory"))
.withColor(ChatFormatting.WHITE)));
}
// Normal title: "Inventory - OwnerName"
component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName()))
.append(Component.translatable("container.inventory"))
.append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName()));
return component;
}
@Override
public @NotNull org.bukkit.inventory.Inventory getBukkitInventory() {
if (bukkitEntity == null) {
bukkitEntity = new OpenPlayerInventory(this);
}
return bukkitEntity;
}
@Override
public void setPlayerOnline(@NotNull org.bukkit.entity.Player player) {
ServerPlayer newOwner = PlayerManager.getHandle(player);
// Only transfer regular inventory - crafting and cursor slots are transient.
newOwner.getInventory().replaceWith(owner.getInventory());
owner = newOwner;
// Update slots to point to new inventory.
slots.forEach(slot -> slot.setHolder(newOwner));
}
@Override
public void setPlayerOffline() {}
@Override
public boolean isInUse() {
return !transaction.isEmpty();
}
@Override
public @NotNull org.bukkit.entity.Player getPlayer() {
return getOwner();
}
@Override
public int getContainerSize() {
return size;
}
@Override
public boolean isEmpty() {
return slots.stream().map(Content::get).allMatch(ItemStack::isEmpty);
}
@Override
public ItemStack getItem(int index) {
return slots.get(index).get();
}
@Override
public ItemStack removeItem(int index, int amount) {
return slots.get(index).removePartial(amount);
}
@Override
public ItemStack removeItemNoUpdate(int index) {
return slots.get(index).remove();
}
@Override
public void setItem(int index, ItemStack itemStack) {
slots.get(index).set(itemStack);
}
@Override
public int getMaxStackSize() {
return maxStackSize;
}
@Override
public void setMaxStackSize(int maxStackSize) {
this.maxStackSize = maxStackSize;
}
@Override
public void setChanged() {}
@Override
public boolean stillValid(Player player) {
return true;
}
@Override
public List<ItemStack> getContents() {
NonNullList<ItemStack> contents = NonNullList.withSize(getContainerSize(), ItemStack.EMPTY);
for (int i = 0; i < getContainerSize(); ++i) {
contents.set(i, getItem(i));
}
return contents;
}
@Override
public void onOpen(CraftHumanEntity viewer) {
transaction.add(viewer);
}
@Override
public void onClose(CraftHumanEntity viewer) {
transaction.remove(viewer);
}
@Override
public List<HumanEntity> getViewers() {
return transaction;
}
@Override
public org.bukkit.entity.Player getOwner() {
return owner.getBukkitEntity();
}
@Override
public Location getLocation() {
return owner.getBukkitEntity().getLocation();
}
@Override
public void clearContent() {
owner.getInventory().clearContent();
owner.inventoryMenu.getCraftSlots().clearContent();
owner.inventoryMenu.slotsChanged(owner.inventoryMenu.getCraftSlots());
owner.containerMenu.setCarried(ItemStack.EMPTY);
}
@Override
public Component getDisplayName() {
return getTitle(null);
}
@Override
public @Nullable AbstractContainerMenu createMenu(int i, Inventory inventory, Player player) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenInventoryMenu(this, serverPlayer, i);
}
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);
}
}
@@ -1,220 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import com.google.common.base.Preconditions;
import net.minecraft.core.NonNullList;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_21_R1.inventory.CraftItemStack;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class OpenPlayerInventory extends CraftInventory implements PlayerInventory {
public OpenPlayerInventory(@NotNull OpenInventory inventory) {
super(inventory);
}
@Override
public OpenInventory getInventory() {
return (OpenInventory) super.getInventory();
}
@Override
public ItemStack[] getContents() {
return asCraftMirror(getInventory().getOwnerHandle().getInventory().getContents());
}
@Override
public void setContents(ItemStack[] items) {
Inventory internal = getInventory().getOwnerHandle().getInventory();
int size = internal.getContainerSize();
Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
for (int index = 0; index < size; ++index) {
if (index < items.length) {
internal.setItem(index, CraftItemStack.asNMSCopy(items[index]));
} else {
internal.setItem(index, net.minecraft.world.item.ItemStack.EMPTY);
}
}
}
@Override
public ItemStack[] getStorageContents() {
return asCraftMirror(getInventory().getOwnerHandle().getInventory().items);
}
@Override
public void setStorageContents(ItemStack[] items) throws IllegalArgumentException {
NonNullList<net.minecraft.world.item.ItemStack> list = getInventory().getOwnerHandle().getInventory().items;
int size = list.size();
Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
for (int index = 0; index < items.length; ++index) {
list.set(index, CraftItemStack.asNMSCopy(items[index]));
}
}
@Override
public @NotNull InventoryType getType() {
return InventoryType.PLAYER;
}
@Override
public @NotNull Player getHolder() {
return getInventory().getOwner();
}
@Override
public @NotNull ItemStack[] getArmorContents() {
return asCraftMirror(getInventory().getOwnerHandle().getInventory().armor);
}
@Override
public void setArmorContents(@Nullable ItemStack[] items) {
NonNullList<net.minecraft.world.item.ItemStack> list = getInventory().getOwnerHandle().getInventory().armor;
int size = list.size();
Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
for (int index = 0; index < items.length; ++index) {
list.set(index, CraftItemStack.asNMSCopy(items[index]));
}
}
@Override
public @NotNull ItemStack[] getExtraContents() {
return asCraftMirror(getInventory().getOwnerHandle().getInventory().offhand);
}
@Override
public void setExtraContents(@Nullable ItemStack[] items) {
NonNullList<net.minecraft.world.item.ItemStack> list = getInventory().getOwnerHandle().getInventory().offhand;
int size = list.size();
Preconditions.checkArgument(items.length <= size, "items.length must be <= %s", size);
for (int index = 0; index < items.length; ++index) {
list.set(index, CraftItemStack.asNMSCopy(items[index]));
}
}
@Override
public @Nullable ItemStack getHelmet() {
return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
.getArmor(EquipmentSlot.HEAD.getIndex()));
}
@Override
public void setHelmet(@Nullable ItemStack helmet) {
getInventory().getOwnerHandle().getInventory().armor
.set(EquipmentSlot.HEAD.getIndex(), CraftItemStack.asNMSCopy(helmet));
}
@Override
public @Nullable ItemStack getChestplate() {
return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
.getArmor(EquipmentSlot.HEAD.getIndex()));
}
@Override
public void setChestplate(@Nullable ItemStack chestplate) {
getInventory().getOwnerHandle().getInventory().armor
.set(EquipmentSlot.CHEST.getIndex(), CraftItemStack.asNMSCopy(chestplate));
}
@Override
public @Nullable ItemStack getLeggings() {
return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
.getArmor(EquipmentSlot.LEGS.getIndex()));
}
@Override
public void setLeggings(@Nullable ItemStack leggings) {
getInventory().getOwnerHandle().getInventory().armor
.set(EquipmentSlot.LEGS.getIndex(), CraftItemStack.asNMSCopy(leggings));
}
@Override
public @Nullable ItemStack getBoots() {
return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory()
.getArmor(EquipmentSlot.FEET.getIndex()));
}
@Override
public void setBoots(@Nullable ItemStack boots) {
getInventory().getOwnerHandle().getInventory().armor
.set(EquipmentSlot.FEET.getIndex(), CraftItemStack.asNMSCopy(boots));
}
@Override
public @NotNull ItemStack getItemInMainHand() {
Inventory internal = getInventory().getOwnerHandle().getInventory();
return CraftItemStack.asCraftMirror(internal.getItem(internal.selected));
}
@Override
public void setItemInMainHand(@Nullable ItemStack item) {
Inventory internal = getInventory().getOwnerHandle().getInventory();
internal.setItem(internal.selected, CraftItemStack.asNMSCopy(item));
}
@Override
public @NotNull ItemStack getItemInOffHand() {
return CraftItemStack.asCraftMirror(getInventory().getOwnerHandle().getInventory().offhand.getFirst());
}
@Override
public void setItemInOffHand(@Nullable ItemStack item) {
getInventory().getOwnerHandle().getInventory().offhand.set(0, CraftItemStack.asNMSCopy(item));
}
@Override
public @NotNull ItemStack getItemInHand() {
return getItemInMainHand();
}
@Override
public void setItemInHand(@Nullable ItemStack stack) {
setItemInMainHand(stack);
}
@Override
public int getHeldItemSlot() {
Inventory internal = getInventory().getOwnerHandle().getInventory();
return internal.items.size() - 9 + internal.selected;
}
@Override
public void setHeldItemSlot(int slot) {
slot %= 9;
getInventory().getOwnerHandle().getInventory().selected = slot;
}
@Override
public @Nullable ItemStack getItem(@NotNull org.bukkit.inventory.EquipmentSlot slot) {
return switch (slot) {
case HAND -> getItemInMainHand();
case OFF_HAND -> getItemInOffHand();
case FEET -> getBoots();
case LEGS -> getLeggings();
case CHEST -> getChestplate();
case HEAD -> getHelmet();
default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot);
};
}
@Override
public void setItem(@NotNull org.bukkit.inventory.EquipmentSlot slot, @Nullable ItemStack item) {
switch (slot) {
case HAND -> setItemInMainHand(item);
case OFF_HAND -> setItemInOffHand(item);
case FEET -> setBoots(item);
case LEGS -> setLeggings(item);
case CHEST -> setChestplate(item);
case HEAD -> setHelmet(item);
default -> throw new IllegalArgumentException("Unsupported EquipmentSlot " + slot);
}
}
}
@@ -1,25 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public class OpenPlayerInventorySelf extends OpenPlayerInventory {
private final int offset;
public OpenPlayerInventorySelf(@NotNull OpenInventory inventory, int offset) {
super(inventory);
this.offset = offset;
}
@Override
public ItemStack getItem(int index) {
return super.getItem(offset + index);
}
@Override
public void setItem(int index, ItemStack item) {
super.setItem(offset + index, item);
}
}
@@ -1,183 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container;
import net.minecraft.core.Registry;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.TagParser;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Unit;
import net.minecraft.world.item.DyeColor;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.item.component.CustomModelData;
import net.minecraft.world.item.component.DyedItemColor;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.entity.BannerPattern;
import net.minecraft.world.level.block.entity.BannerPatternLayers;
import net.minecraft.world.level.block.entity.BannerPatterns;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.craftbukkit.v1_21_R1.CraftRegistry;
import org.jetbrains.annotations.NotNull;
import java.util.EnumMap;
import java.util.List;
import java.util.Optional;
public final class Placeholders {
private static final CustomModelData DEFAULT_CUSTOM_MODEL_DATA = new CustomModelData(9999);
public static final @NotNull EnumMap<GameType, ItemStack> BLOCKED_GAME_TYPE = new EnumMap<>(GameType.class);
public static @NotNull ItemStack craftingOutput = defaultCraftingOutput();
public static @NotNull ItemStack cursor = defaultCursor();
public static @NotNull ItemStack drop = defaultDrop();
public static @NotNull ItemStack emptyHelmet = getEmptyArmor(Items.LEATHER_HELMET);
public static @NotNull ItemStack emptyChestplate = getEmptyArmor(Items.LEATHER_CHESTPLATE);
public static @NotNull ItemStack emptyLeggings = getEmptyArmor(Items.LEATHER_LEGGINGS);
public static @NotNull ItemStack emptyBoots = getEmptyArmor(Items.LEATHER_BOOTS);
public static @NotNull ItemStack emptyOffHand = getEmptyShield();
public static @NotNull ItemStack notSlot = defaultNotSlot();
public static @NotNull ItemStack blockedOffline = defaultBlockedOffline();
static {
for (GameType type : GameType.values()) {
// Barrier: "Not available - Creative" etc.
ItemStack typeItem = new ItemStack(Items.BARRIER);
typeItem.set(
DataComponents.ITEM_NAME,
Component.translatable("options.narrator.notavailable").append(" - ").append(type.getShortDisplayName()));
BLOCKED_GAME_TYPE.put(type, typeItem);
}
}
public static void load(@NotNull ConfigurationSection section) throws Exception {
craftingOutput = parse(section, "crafting-output", craftingOutput);
cursor = parse(section, "cursor", cursor);
drop = parse(section, "drop", drop);
emptyHelmet = parse(section, "empty-helmet", emptyHelmet);
emptyChestplate = parse(section, "empty-chestplate", emptyChestplate);
emptyLeggings = parse(section, "empty-leggings", emptyLeggings);
emptyBoots = parse(section, "empty-boots", emptyBoots);
emptyOffHand = parse(section, "empty-off-hand", emptyOffHand);
notSlot = parse(section, "not-a-slot", notSlot);
blockedOffline = parse(section, "blocked.offline", blockedOffline);
BLOCKED_GAME_TYPE.put(GameType.CREATIVE, parse(section, "blocked.creative", BLOCKED_GAME_TYPE.get(GameType.CREATIVE)));
BLOCKED_GAME_TYPE.put(GameType.SPECTATOR, parse(section, "blocked.spectator", BLOCKED_GAME_TYPE.get(GameType.SPECTATOR)));
}
private static @NotNull ItemStack parse(
@NotNull ConfigurationSection section,
@NotNull String path,
@NotNull ItemStack defaultStack) throws Exception {
String itemText = section.getString(path);
if (itemText == null) {
return defaultStack;
}
CompoundTag compoundTag = TagParser.parseTag(itemText);
Optional<ItemStack> parsed = ItemStack.parse(CraftRegistry.getMinecraftRegistry(), compoundTag);
return parsed.filter(itemStack -> !itemStack.isEmpty()).orElse(defaultStack);
}
public static ItemStack survivalOnly(@NotNull ServerPlayer serverPlayer) {
if (serverPlayer.connection == null || serverPlayer.connection.isDisconnected()) {
return blockedOffline;
}
return BLOCKED_GAME_TYPE.getOrDefault(serverPlayer.gameMode.getGameModeForPlayer(), ItemStack.EMPTY);
}
private static ItemStack defaultCraftingOutput() {
// Crafting table: "Crafting"
ItemStack itemStack = new ItemStack(Items.CRAFTING_TABLE);
itemStack.set(DataComponents.ITEM_NAME, Component.translatable("container.crafting"));
itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
return itemStack;
}
private static ItemStack defaultCursor() {
// Cursor-like banner with no tooltip
ItemStack itemStack = new ItemStack(Items.WHITE_BANNER);
RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry();
Registry<BannerPattern> bannerPatterns = minecraftRegistry.registryOrThrow(Registries.BANNER_PATTERN);
BannerPattern halfDiagBottomRight = bannerPatterns.getOrThrow(BannerPatterns.DIAGONAL_RIGHT);
BannerPattern downRight = bannerPatterns.getOrThrow(BannerPatterns.STRIPE_DOWNRIGHT);
BannerPattern border = bannerPatterns.getOrThrow(BannerPatterns.BORDER);
itemStack.set(DataComponents.BANNER_PATTERNS,
new BannerPatternLayers(List.of(
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfDiagBottomRight), DyeColor.GRAY),
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(downRight), DyeColor.WHITE),
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(border), DyeColor.GRAY))));
itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
return itemStack;
}
private static ItemStack defaultDrop() {
// Dropper: "Drop Selected Item"
ItemStack itemStack = new ItemStack(Items.DROPPER);
// Note: translatable component, not keybind component! We want the text identifying the keybind, not the key.
itemStack.set(DataComponents.ITEM_NAME, Component.translatable("key.drop"));
itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
return itemStack;
}
private static ItemStack getEmptyArmor(ItemLike item) {
// Inventory-background-grey-ish leather armor with no tooltip
ItemStack itemStack = new ItemStack(item);
DyedItemColor color = new DyedItemColor(0xC8C8C8, false);
itemStack.set(DataComponents.DYED_COLOR, color);
itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
return itemStack;
}
private static ItemStack getEmptyShield() {
ItemStack itemStack = new ItemStack(Items.SHIELD);
itemStack.set(DataComponents.BASE_COLOR, DyeColor.MAGENTA);
RegistryAccess minecraftRegistry = CraftRegistry.getMinecraftRegistry();
Registry<BannerPattern> bannerPatterns = minecraftRegistry.registryOrThrow(Registries.BANNER_PATTERN);
BannerPattern halfLeft = bannerPatterns.getOrThrow(BannerPatterns.HALF_VERTICAL);
BannerPattern topLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_LEFT);
BannerPattern topRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_TOP_RIGHT);
BannerPattern bottomLeft = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_LEFT);
BannerPattern bottomRight = bannerPatterns.getOrThrow(BannerPatterns.SQUARE_BOTTOM_RIGHT);
itemStack.set(DataComponents.BANNER_PATTERNS,
new BannerPatternLayers(List.of(
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(halfLeft), DyeColor.BLACK),
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topLeft), DyeColor.MAGENTA),
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomLeft), DyeColor.MAGENTA),
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(topRight), DyeColor.BLACK),
new BannerPatternLayers.Layer(bannerPatterns.wrapAsHolder(bottomRight), DyeColor.BLACK))));
itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
return itemStack;
}
private static ItemStack defaultNotSlot() {
// White pane with no tooltip
ItemStack itemStack = new ItemStack(Items.WHITE_STAINED_GLASS_PANE);
itemStack.set(DataComponents.HIDE_TOOLTIP, Unit.INSTANCE);
itemStack.set(DataComponents.CUSTOM_MODEL_DATA, DEFAULT_CUSTOM_MODEL_DATA);
return itemStack;
}
private static ItemStack defaultBlockedOffline() {
// Barrier: "Not available - Offline"
ItemStack itemStack = new ItemStack(Items.BARRIER);
itemStack.set(DataComponents.ITEM_NAME,
Component.translatable("options.narrator.notavailable")
.append(Component.literal(" - "))
.append(Component.translatable("gui.socialInteractions.status_offline")));
return itemStack;
}
private Placeholders() {
throw new IllegalStateException("Cannot create instance of utility class.");
}
}
@@ -1,69 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
/**
* An interface defining behaviors for entries in a {@link Container}. Used to reduce duplicate content reordering.
*/
public interface Content {
/**
* Update internal holder.
*
* @param holder the new holder
*/
void setHolder(@NotNull ServerPlayer holder);
/**
* Get the current item.
*
* @return the current item
*/
ItemStack get();
/**
* Remove the current item.
*
* @return the current item
*/
ItemStack remove();
/**
* Remove some of the current item.
*
* @return the current item
*/
ItemStack removePartial(int amount);
/**
* Set the current item. If slot is currently not usable, will drop item instead.
*
* @param itemStack the item to set
*/
void set(ItemStack itemStack);
/**
* Get a {@link Slot} for use in a {@link net.minecraft.world.inventory.AbstractContainerMenu ContainerMenu}. Will
* impose any specific restrictions to insertion or removal.
*
* @param container the backing container
* @param slot the slot of the backing container represented
* @param x clientside x dimension from top left of inventory, not used
* @param y clientside y dimension from top left of inventory, not used
* @return a menu slot
*/
Slot asSlot(Container container, int slot, int x, int y);
/**
* Get a loose Bukkit translation of what this slot stores. For example, any slot that drops items at the owner rather
* than insert them will report itself as being {@link org.bukkit.event.inventory.InventoryType.SlotType#OUTSIDE}.
*
* @return the closes Bukkit slot type
*/
org.bukkit.event.inventory.InventoryType.SlotType getSlotType();
}
@@ -1,131 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* A slot in a survival crafting inventory. Unavailable when not online in a survival mode.
*/
public class ContentCrafting implements Content {
private final int index;
private ServerPlayer holder;
private List<ItemStack> items;
public ContentCrafting(@NotNull ServerPlayer holder, int index) {
setHolder(holder);
this.index = index;
}
private boolean isAvailable() {
return isAvailable(holder);
}
static boolean isAvailable(@NotNull ServerPlayer holder) {
// Player must be online and not in creative - since the creative client is (semi-)authoritative,
// it ignores changes without extra help, and will delete the item as a result.
// Spectator mode is technically possible but may cause the item to be dropped if the client opens an inventory.
return holder.connection != null && !holder.connection.isDisconnected() && holder.gameMode.isSurvival();
}
@Override
public void setHolder(@NotNull ServerPlayer holder) {
this.holder = holder;
// Note: CraftingContainer#getItems is immutable! Be careful with updates.
this.items = holder.inventoryMenu.getCraftSlots().getContents();
}
@Override
public ItemStack get() {
return isAvailable() ? items.get(index) : ItemStack.EMPTY;
}
@Override
public ItemStack remove() {
if (!this.isAvailable()) {
return ItemStack.EMPTY;
}
ItemStack removed = items.remove(index);
if (removed.isEmpty()) {
return ItemStack.EMPTY;
}
holder.inventoryMenu.slotsChanged(holder.inventoryMenu.getCraftSlots());
return removed;
}
@Override
public ItemStack removePartial(int amount) {
if (!this.isAvailable()) {
return ItemStack.EMPTY;
}
ItemStack removed = ContainerHelper.removeItem(items, index, amount);
if (removed.isEmpty()) {
return ItemStack.EMPTY;
}
holder.inventoryMenu.slotsChanged(holder.inventoryMenu.getCraftSlots());
return removed;
}
@Override
public void set(ItemStack itemStack) {
if (isAvailable()) {
items.set(index, itemStack);
holder.inventoryMenu.slotsChanged(holder.inventoryMenu.getCraftSlots());
} else {
holder.drop(itemStack, false);
}
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotCrafting(container, slot, x, y);
}
@Override
public InventoryType.SlotType getSlotType() {
return isAvailable() ? InventoryType.SlotType.CRAFTING : InventoryType.SlotType.OUTSIDE;
}
public class SlotCrafting extends SlotPlaceholder {
private SlotCrafting(Container container, int index, int x, int y) {
super(container, index, x, y);
}
@Override
public ItemStack getOrDefault() {
return isAvailable() ? items.get(ContentCrafting.this.index) : Placeholders.survivalOnly(holder);
}
@Override
public boolean mayPickup(Player player) {
return isAvailable();
}
@Override
public boolean mayPlace(ItemStack itemStack) {
return isAvailable();
}
@Override
public boolean hasItem() {
return isAvailable() && super.hasItem();
}
@Override
public boolean isFake() {
return !isAvailable();
}
}
}
@@ -1,48 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.InventoryMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
/**
* A slot allowing viewing of the crafting result.
*
* <p>Unmodifiable because I said so. Use your own crafting grid.</p>
*/
public class ContentCraftingResult extends ContentViewOnly {
public ContentCraftingResult(@NotNull ServerPlayer holder) {
super(holder);
}
@Override
public ItemStack get() {
InventoryMenu inventoryMenu = holder.inventoryMenu;
return inventoryMenu.getSlot(inventoryMenu.getResultSlotIndex()).getItem();
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotViewOnly(container, slot, x, y) {
@Override
public ItemStack getOrDefault() {
if (!ContentCrafting.isAvailable(holder)) {
return Placeholders.survivalOnly(holder);
}
InventoryMenu inventoryMenu = holder.inventoryMenu;
return inventoryMenu.getSlot(inventoryMenu.getResultSlotIndex()).getItem();
}
};
}
@Override
public InventoryType.SlotType getSlotType() {
return InventoryType.SlotType.RESULT;
}
}
@@ -1,117 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
/**
* A slot wrapping the active menu's cursor. Unavailable when not online in a survival mode.
*/
public class ContentCursor implements Content {
private @NotNull ServerPlayer holder;
public ContentCursor(@NotNull ServerPlayer holder) {
this.holder = holder;
}
@Override
public void setHolder(@NotNull ServerPlayer holder) {
this.holder = holder;
}
@Override
public ItemStack get() {
return isAvailable() ? holder.containerMenu.getCarried() : ItemStack.EMPTY;
}
@Override
public ItemStack remove() {
ItemStack carried = holder.containerMenu.getCarried();
holder.containerMenu.setCarried(ItemStack.EMPTY);
return carried;
}
@Override
public ItemStack removePartial(int amount) {
ItemStack carried = holder.containerMenu.getCarried();
if (!carried.isEmpty() && carried.getCount() >= amount) {
ItemStack value = carried.split(amount);
if (carried.isEmpty()) {
holder.containerMenu.setCarried(ItemStack.EMPTY);
}
return value;
}
return ItemStack.EMPTY;
}
@Override
public void set(ItemStack itemStack) {
if (isAvailable()) {
holder.containerMenu.setCarried(itemStack);
} else {
holder.drop(itemStack, false);
}
}
private boolean isAvailable() {
// Player must be online and not in creative - since the creative client is (semi-)authoritative,
// it ignores changes without extra help, and will delete the item as a result.
// Spectator mode is technically possible but may cause the item to be dropped if the client opens an inventory.
return holder.connection != null && !holder.connection.isDisconnected() && holder.gameMode.isSurvival();
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotCursor(container, slot, x, y);
}
@Override
public InventoryType.SlotType getSlotType() {
// As close as possible to "not real"
return InventoryType.SlotType.OUTSIDE;
}
public class SlotCursor extends SlotPlaceholder {
private SlotCursor(Container container, int index, int x, int y) {
super(container, index, x, y);
}
@Override
public ItemStack getOrDefault() {
if (!isAvailable()) {
return Placeholders.survivalOnly(holder);
}
ItemStack carried = holder.containerMenu.getCarried();
return carried.isEmpty() ? Placeholders.cursor : carried;
}
@Override
public boolean mayPickup(Player player) {
return isAvailable();
}
@Override
public boolean mayPlace(ItemStack itemStack) {
return isAvailable();
}
@Override
public boolean hasItem() {
return isAvailable() && super.hasItem();
}
@Override
public boolean isFake() {
return true;
}
}
}
@@ -1,88 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
/**
* A fake slot used to drop items. Unavailable offline.
*/
public class ContentDrop implements Content {
private ServerPlayer holder;
public ContentDrop(@NotNull ServerPlayer holder) {
this.holder = holder;
}
@Override
public void setHolder(@NotNull ServerPlayer holder) {
this.holder = holder;
}
@Override
public ItemStack get() {
return ItemStack.EMPTY;
}
@Override
public ItemStack remove() {
return ItemStack.EMPTY;
}
@Override
public ItemStack removePartial(int amount) {
return ItemStack.EMPTY;
}
@Override
public void set(ItemStack itemStack) {
holder.drop(itemStack, true);
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotDrop(container, slot, x, y);
}
@Override
public InventoryType.SlotType getSlotType() {
// Behaves like dropping an item outside the screen, just by the target player.
return InventoryType.SlotType.OUTSIDE;
}
public class SlotDrop extends SlotPlaceholder {
private SlotDrop(Container container, int index, int x, int y) {
super(container, index, x, y);
}
@Override
public ItemStack getOrDefault() {
return holder.connection != null && !holder.connection.isDisconnected()
? Placeholders.drop
: Placeholders.blockedOffline;
}
@Override
public boolean mayPlace(ItemStack itemStack) {
return holder.connection != null && !holder.connection.isDisconnected();
}
@Override
public boolean hasItem() {
return false;
}
@Override
public boolean isFake() {
return true;
}
}
}
@@ -1,78 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
/**
* A slot for equipment that displays placeholders if empty.
*/
public class ContentEquipment extends ContentList {
private final ItemStack placeholder;
private final EquipmentSlot equipmentSlot;
public ContentEquipment(ServerPlayer holder, int index, EquipmentSlot equipmentSlot) {
super(holder, index, InventoryType.SlotType.ARMOR);
placeholder = switch (equipmentSlot) {
case HEAD -> Placeholders.emptyHelmet;
case CHEST -> Placeholders.emptyChestplate;
case LEGS -> Placeholders.emptyLeggings;
case FEET -> Placeholders.emptyBoots;
default -> Placeholders.emptyOffHand;
};
this.equipmentSlot = equipmentSlot;
}
@Override
public void setHolder(@NotNull ServerPlayer holder) {
this.items = holder.getInventory().armor;
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotEquipment(container, slot, x, y);
}
public class SlotEquipment extends SlotPlaceholder {
private ServerPlayer viewer;
SlotEquipment(Container container, int index, int x, int y) {
super(container, index, x, y);
}
@Override
public ItemStack getOrDefault() {
ItemStack itemStack = getItem();
if (!itemStack.isEmpty()) {
return itemStack;
}
return placeholder;
}
public EquipmentSlot getEquipmentSlot() {
return equipmentSlot;
}
public void onlyEquipmentFor(ServerPlayer viewer) {
this.viewer = viewer;
}
@Override
public boolean mayPlace(ItemStack var0) {
if (viewer == null) {
return true;
}
return equipmentSlot == EquipmentSlot.OFFHAND || viewer.getEquipmentSlotForItem(var0) == equipmentSlot;
}
}
}
@@ -1,58 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import java.util.List;
/**
* A normal slot backed by an item list.
*/
public abstract class ContentList implements Content {
private final int index;
private final InventoryType.SlotType slotType;
protected List<ItemStack> items;
public ContentList(ServerPlayer holder, int index, InventoryType.SlotType slotType) {
this.index = index;
this.slotType = slotType;
setHolder(holder);
}
@Override
public ItemStack get() {
return items.get(index);
}
@Override
public ItemStack remove() {
ItemStack removed = items.remove(index);
return removed == null || removed.isEmpty() ? ItemStack.EMPTY : removed;
}
@Override
public ItemStack removePartial(int amount) {
return ContainerHelper.removeItem(items, index, amount);
}
@Override
public void set(ItemStack itemStack) {
items.set(index, itemStack);
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new Slot(container, slot, x, y);
}
@Override
public InventoryType.SlotType getSlotType() {
return slotType;
}
}
@@ -1,46 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.inventory.Slot;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
/**
* A slot for equipment that updates held items if necessary.
*/
public class ContentOffHand extends ContentEquipment {
private ServerPlayer holder;
public ContentOffHand(ServerPlayer holder, int localIndex) {
super(holder, localIndex, EquipmentSlot.OFFHAND);
}
@Override
public void setHolder(@NotNull ServerPlayer holder) {
this.items = holder.getInventory().offhand;
this.holder = holder;
}
@Override
public InventoryType.SlotType getSlotType() {
return InventoryType.SlotType.QUICKBAR;
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotEquipment(container, slot, x, y) {
@Override
public void setChanged() {
if (holder.connection != null
&& !holder.connection.isDisconnected()
&& holder.containerMenu != holder.inventoryMenu) {
holder.resendItemInHands();
}
}
};
}
}
@@ -1,56 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
/**
* A view-only slot that can't be interacted with.
*/
public class ContentViewOnly implements Content {
@NotNull ServerPlayer holder;
public ContentViewOnly(@NotNull ServerPlayer holder) {
this.holder = holder;
}
@Override
public void setHolder(@NotNull ServerPlayer holder) {
this.holder = holder;
}
@Override
public ItemStack get() {
return ItemStack.EMPTY;
}
@Override
public ItemStack remove() {
return ItemStack.EMPTY;
}
@Override
public ItemStack removePartial(int amount) {
return ItemStack.EMPTY;
}
@Override
public void set(ItemStack itemStack) {
this.holder.drop(itemStack, false);
}
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotViewOnly(container, slot, x, y);
}
@Override
public InventoryType.SlotType getSlotType() {
return InventoryType.SlotType.OUTSIDE;
}
}
@@ -1,20 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
/**
* An implementation of a slot as used by a menu that may have fake placeholder items.
*
* <p>Used to prevent plugins (particularly sorting plugins) from adding placeholders to inventories.</p>
*/
public abstract class SlotPlaceholder extends Slot {
public SlotPlaceholder(Container container, int index, int x, int y) {
super(container, index, x, y);
}
public abstract ItemStack getOrDefault();
}
@@ -1,151 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.container.slot;
import com.lishid.openinv.internal.v1_21_R1.container.Placeholders;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Optional;
/**
* A view-only {@link Slot}. "Blank" by default, but can wrap another slot to display its content.
*/
public class SlotViewOnly extends SlotPlaceholder {
public static @NotNull SlotViewOnly wrap(@NotNull Slot wrapped) {
SlotViewOnly wrapper;
if (wrapped instanceof SlotPlaceholder placeholder) {
wrapper = new SlotViewOnly(wrapped.container, wrapped.slot, wrapped.x, wrapped.y) {
@Override
public ItemStack getOrDefault() {
return placeholder.getOrDefault();
}
};
} else {
wrapper = new SlotViewOnly(wrapped.container, wrapped.slot, wrapped.x, wrapped.y) {
@Override
public ItemStack getOrDefault() {
return wrapped.getItem();
}
};
}
wrapper.index = wrapped.index;
return wrapper;
}
public SlotViewOnly(Container container, int index, int x, int y) {
super(container, index, x, y);
}
@Override
public ItemStack getOrDefault() {
return Placeholders.notSlot;
}
@Override
public void onQuickCraft(ItemStack var0, ItemStack var1) {
}
@Override
public void onTake(Player var0, ItemStack var1) {
}
@Override
public boolean mayPlace(ItemStack var0) {
return false;
}
@Override
public ItemStack getItem() {
return ItemStack.EMPTY;
}
@Override
public boolean hasItem() {
return false;
}
@Override
public void setByPlayer(ItemStack newStack) {
}
@Override
public void setByPlayer(ItemStack newStack, ItemStack oldStack) {
}
@Override
public void set(ItemStack var0) {
}
@Override
public void setChanged() {
}
@Override
public int getMaxStackSize() {
return 0;
}
@Override
public int getMaxStackSize(ItemStack itemStack) {
return 0;
}
@Override
public ItemStack remove(int amount) {
return ItemStack.EMPTY;
}
@Override
public boolean mayPickup(Player var0) {
return false;
}
@Override
public boolean isActive() {
return false;
}
@Override
public Optional<ItemStack> tryRemove(int var0, int var1, Player var2) {
return Optional.empty();
}
@Override
public ItemStack safeTake(int var0, int var1, Player var2) {
return ItemStack.EMPTY;
}
@Override
public ItemStack safeInsert(ItemStack itemStack) {
return itemStack;
}
@Override
public ItemStack safeInsert(ItemStack itemStack, int amount) {
return itemStack;
}
@Override
public boolean allowModification(Player var0) {
return false;
}
@Override
public int getContainerSlot() {
return this.slot;
}
@Override
public boolean isHighlightable() {
return false;
}
@Override
public boolean isFake() {
return true;
}
}
@@ -1,177 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.player;
import com.lishid.openinv.event.OpenEvents;
import com.mojang.logging.LogUtils;
import net.minecraft.Util;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NumericTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.storage.PlayerDataStorage;
import org.bukkit.craftbukkit.v1_21_R1.CraftServer;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
public class OpenPlayer extends CraftPlayer {
/**
* List of tags to always reset when saving.
*
* @see net.minecraft.world.entity.Entity#saveWithoutId(CompoundTag)
* @see net.minecraft.server.level.ServerPlayer#addAdditionalSaveData(CompoundTag)
* @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(CompoundTag)
* @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(CompoundTag)
*/
private static final Set<String> RESET_TAGS = Set.of(
// Entity#saveWithoutId(CompoundTag)
"CustomName",
"CustomNameVisible",
"Silent",
"NoGravity",
"Glowing",
"TicksFrozen",
"HasVisualFire",
"Tags",
"Passengers",
// ServerPlayer#addAdditionalSaveData(CompoundTag)
// Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle
"warden_spawn_tracker",
"enteredNetherPosition",
"SpawnX",
"SpawnY",
"SpawnZ",
"SpawnForced",
"SpawnAngle",
"SpawnDimension",
"raid_omen_position",
// Player#addAdditionalSaveData(CompoundTag)
"ShoulderEntityLeft",
"ShoulderEntityRight",
"LastDeathLocation",
"current_explosion_impact_pos",
// LivingEntity#addAdditionalSaveData(CompoundTag)
"active_effects",
"SleepingX",
"SleepingY",
"SleepingZ",
"Brain"
);
private final PlayerManager manager;
OpenPlayer(CraftServer server, ServerPlayer entity, PlayerManager manager) {
super(server, entity);
this.manager = manager;
}
@Override
public void loadData() {
manager.loadData(getHandle());
}
@Override
public void saveData() {
if (OpenEvents.saveCancelled(this)) {
return;
}
ServerPlayer player = this.getHandle();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try {
PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
CompoundTag oldData = isOnline() ? null : worldNBTStorage.load(player.getName().getString(), player.getStringUUID()).orElse(null);
CompoundTag playerData = getWritableTag(oldData);
playerData = player.saveWithoutId(playerData);
setExtraData(playerData);
if (oldData != null) {
// Revert certain special data values when offline.
revertSpecialValues(playerData, oldData);
}
Path playerDataDir = worldNBTStorage.getPlayerDir().toPath();
Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
NbtIo.writeCompressed(playerData, tempFile);
Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
Util.safeReplaceFile(dataFile, tempFile, backupFile);
} catch (Exception e) {
LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
}
}
@Contract("null -> new")
private @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
if (oldData == null) {
return new CompoundTag();
}
// Copy old data. This is a deep clone, so operating on it should be safe.
oldData = oldData.copy();
// Remove vanilla/server data that is not written every time.
oldData.getAllKeys()
.removeIf(key -> RESET_TAGS.contains(key) || key.startsWith("Bukkit"));
return oldData;
}
private void revertSpecialValues(@NotNull CompoundTag newData, @NotNull CompoundTag oldData) {
// Revert automatic updates to play timestamps.
copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastLogin", NumericTag.class);
}
private <T extends Tag> void copyValue(
@NotNull CompoundTag source,
@NotNull CompoundTag target,
@NotNull String container,
@NotNull String key,
@SuppressWarnings("SameParameterValue") @NotNull Class<T> tagType) {
CompoundTag oldContainer = getTag(source, container, CompoundTag.class);
CompoundTag newContainer = getTag(target, container, CompoundTag.class);
// New container being null means the server implementation doesn't store this data.
if (newContainer == null) {
return;
}
// If old tag exists, copy it to new location, removing otherwise.
setTag(newContainer, key, getTag(oldContainer, key, tagType));
}
private <T extends Tag> @Nullable T getTag(
@Nullable CompoundTag container,
@NotNull String key,
@NotNull Class<T> dataType) {
if (container == null) {
return null;
}
Tag value = container.get(key);
if (value == null || !dataType.isAssignableFrom(value.getClass())) {
return null;
}
return dataType.cast(value);
}
private <T extends Tag> void setTag(
@NotNull CompoundTag container,
@NotNull String key,
@Nullable T data) {
if (data == null) {
container.remove(key);
} else {
container.put(key, data);
}
}
}
@@ -1,272 +0,0 @@
package com.lishid.openinv.internal.v1_21_R1.player;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.v1_21_R1.container.OpenEnderChest;
import com.lishid.openinv.internal.v1_21_R1.container.OpenInventory;
import com.mojang.authlib.GameProfile;
import com.mojang.serialization.Dynamic;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ClientInformation;
import net.minecraft.server.level.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;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.dimension.DimensionType;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.craftbukkit.v1_21_R1.CraftServer;
import org.bukkit.craftbukkit.v1_21_R1.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R1.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R1.event.CraftEventFactory;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Field;
import java.util.UUID;
import java.util.logging.Logger;
public class PlayerManager implements com.lishid.openinv.internal.PlayerManager {
private static boolean paper;
static {
try {
Class.forName("io.papermc.paper.configuration.Configuration");
paper = true;
} catch (ClassNotFoundException ignored) {
paper = false;
}
}
private final @NotNull Logger logger;
private @Nullable Field bukkitEntity;
public PlayerManager(@NotNull Logger logger) {
this.logger = logger;
try {
bukkitEntity = Entity.class.getDeclaredField("bukkitEntity");
} catch (NoSuchFieldException e) {
logger.warning("Unable to obtain field to inject custom save process - certain player data may be lost when saving!");
logger.log(java.util.logging.Level.WARNING, e.getMessage(), e);
bukkitEntity = null;
}
}
public static @NotNull ServerPlayer getHandle(final Player player) {
if (player instanceof CraftPlayer) {
return ((CraftPlayer) player).getHandle();
}
Server server = player.getServer();
ServerPlayer nmsPlayer = null;
if (server instanceof CraftServer) {
nmsPlayer = ((CraftServer) server).getHandle().getPlayer(player.getUniqueId());
}
if (nmsPlayer == null) {
// Could use reflection to examine fields, but it's honestly not worth the bother.
throw new RuntimeException("Unable to fetch EntityPlayer from Player implementation " + player.getClass().getName());
}
return nmsPlayer;
}
@Override
public @Nullable Player loadPlayer(@NotNull final OfflinePlayer offline) {
// Ensure player has data
if (!offline.hasPlayedBefore()) {
return null;
}
MinecraftServer server = ((CraftServer) Bukkit.getServer()).getServer();
ServerLevel worldServer = server.getLevel(Level.OVERWORLD);
if (worldServer == null) {
return null;
}
// Create a new ServerPlayer.
ServerPlayer entity = createNewPlayer(server, worldServer, offline);
// Stop listening for advancement progression - if this is not cleaned up, loading causes a memory leak.
entity.getAdvancements().stopListening();
// Try to load the player's data.
if (loadData(entity)) {
// If data is loaded successfully, return the Bukkit entity.
return entity.getBukkitEntity();
}
return null;
}
private @NotNull ServerPlayer createNewPlayer(
@NotNull MinecraftServer server,
@NotNull ServerLevel worldServer,
@NotNull final OfflinePlayer offline) {
// See net.minecraft.server.players.PlayerList#canPlayerLogin(ServerLoginPacketListenerImpl, GameProfile)
// See net.minecraft.server.network.ServerLoginPacketListenerImpl#handleHello(ServerboundHelloPacket)
GameProfile profile = new GameProfile(offline.getUniqueId(),
offline.getName() != null ? offline.getName() : offline.getUniqueId().toString());
ClientInformation dummyInfo = new ClientInformation(
"en_us",
1, // Reduce distance just in case.
ChatVisiblity.HIDDEN, // Don't accept chat.
false,
ServerPlayer.DEFAULT_MODEL_CUSTOMIZATION,
ServerPlayer.DEFAULT_MAIN_HAND,
true,
false // Don't list in player list (not that this player is in the list anyway).
);
ServerPlayer entity = new ServerPlayer(server, worldServer, profile, dummyInfo);
try {
injectPlayer(entity);
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!");
}
return entity;
}
boolean loadData(@NotNull ServerPlayer player) {
// See CraftPlayer#loadData
CompoundTag loadedData = player.server.getPlayerList().playerIo.load(player).orElse(null);
if (loadedData == null) {
// Exceptions with loading are logged by Mojang.
return false;
}
// Read basic data into the player.
player.load(loadedData);
// Also read "extra" data.
player.readAdditionalSaveData(loadedData);
// Game type settings are also loaded separately.
player.loadGameTypes(loadedData);
if (paper) {
// Paper: world is not loaded by ServerPlayer#load(CompoundTag).
parseWorld(player, loadedData);
}
return true;
}
private void parseWorld(@NotNull ServerPlayer player, @NotNull CompoundTag loadedData) {
// See PlayerList#placeNewPlayer
World bukkitWorld;
if (loadedData.contains("WorldUUIDMost") && loadedData.contains("WorldUUIDLeast")) {
// Modern Bukkit world.
bukkitWorld = Bukkit.getServer().getWorld(new UUID(loadedData.getLong("WorldUUIDMost"), loadedData.getLong("WorldUUIDLeast")));
} else if (loadedData.contains("world", net.minecraft.nbt.Tag.TAG_STRING)) {
// Legacy Bukkit world.
bukkitWorld = Bukkit.getServer().getWorld(loadedData.getString("world"));
} else {
// Vanilla player data.
DimensionType.parseLegacy(new Dynamic<>(NbtOps.INSTANCE, loadedData.get("Dimension")))
.resultOrPartial(logger::warning)
.map(player.server::getLevel)
// If ServerLevel exists, set, otherwise move to spawn.
.ifPresentOrElse(player::setServerLevel, () -> player.spawnIn(null));
return;
}
if (bukkitWorld == null) {
player.spawnIn(null);
return;
}
player.setServerLevel(((CraftWorld) bukkitWorld).getHandle());
}
private void injectPlayer(ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(player.server.server, player, this));
}
@Override
public @NotNull Player inject(@NotNull Player player) {
try {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.getBukkitEntity() instanceof OpenPlayer openPlayer) {
return openPlayer;
}
injectPlayer(nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!");
return player;
}
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player bukkitPlayer, @NotNull ISpecialInventory inventory) {
ServerPlayer player = getHandle(bukkitPlayer);
if (player.connection == null) {
return null;
}
MenuProvider provider;
Component title;
if (inventory instanceof OpenInventory playerInv) {
provider = playerInv;
title = playerInv.getTitle(player);
} else if (inventory instanceof OpenEnderChest enderChest) {
provider = enderChest;
title = enderChest.getDisplayName();
} 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.
menu.setTitle(title);
menu = CraftEventFactory.callInventoryOpenEvent(player, menu, false);
// Menu is null if event is cancelled.
if (menu == null) {
return null;
}
player.containerMenu = menu;
player.connection.send(new ClientboundOpenScreenPacket(menu.containerId, menu.getType(), menu.getTitle()));
player.initMenu(menu);
return menu.getBukkitView();
}
}
+11 -90
View File
@@ -1,5 +1,5 @@
<!--
~ Copyright (C) 2011-2022 lishid. All rights reserved.
~ Copyright (C) 2011-2021 lishid. All rights reserved.
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
@@ -21,55 +21,32 @@
<parent>
<artifactId>openinvparent</artifactId>
<groupId>com.lishid</groupId>
<version>5.0.0</version>
<version>4.1.9-SNAPSHOT</version>
</parent>
<artifactId>openinvplugin</artifactId>
<artifactId>openinvplugincore</artifactId>
<name>OpenInvPlugin</name>
<dependencies>
<dependency>
<artifactId>spigot-api</artifactId>
<groupId>org.spigotmc</groupId>
</dependency>
<dependency>
<artifactId>planarwrappers</artifactId>
<groupId>com.github.jikoo</groupId>
</dependency>
<dependency>
<artifactId>openinvapi</artifactId>
<groupId>com.lishid</groupId>
</dependency>
<dependency>
<artifactId>openinvcommon</artifactId>
<groupId>com.lishid</groupId>
</dependency>
<dependency>
<groupId>com.lishid</groupId>
<artifactId>openinvadapter1_21_R1</artifactId>
<version>5.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.lishid</groupId>
<artifactId>openinvadapter1_20_R4</artifactId>
<version>5.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.lishid</groupId>
<artifactId>openinvadapter1_20_R3</artifactId>
<version>5.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<artifactId>annotations</artifactId>
<groupId>org.jetbrains</groupId>
</dependency>
<dependency>
<artifactId>paper-api</artifactId>
<groupId>io.papermc.paper</groupId>
</dependency>
<dependency>
<artifactId>planarwrappers</artifactId>
<groupId>com.github.jikoo</groupId>
</dependency>
</dependencies>
<build>
<finalName>OpenInv</finalName>
<resources>
<resource>
<directory>src/main/resources</directory>
@@ -80,66 +57,10 @@
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<filters>
<filter>
<!-- Always shade entirety of required modules. -->
<artifact>com.lishid:openinv*</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<!-- Don't warn about file conflicts that'll be clobbered anyway. -->
<excludes>
<exclude>META-INF/MANIFEST.MF</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>com.github.jikoo.planarwrappers</pattern>
<shadedPattern>com.github.jikoo.openinv.planarwrappers</shadedPattern>
</relocation>
</relocations>
<minimizeJar>true</minimizeJar>
</configuration>
</plugin>
<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>
@@ -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,219 +0,0 @@
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.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.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 {
@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) {
if (handleInventoryInteract(event)) {
return;
}
// Safe cast - has to be a player to be the holder of a special player inventory.
Player player = (Player) event.getWhoClicked();
if (event.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY) {
return;
}
// Extra handling for MOVE_TO_OTHER_INVENTORY - apparently Mojang no longer removes the item from the target
// inventory prior to adding it to existing stacks.
ItemStack currentItem = event.getCurrentItem();
if (currentItem == null) {
// Other plugin doing some sort of handling (would be NOTHING for null item otherwise), ignore.
return;
}
ItemStack clone = currentItem.clone();
event.setCurrentItem(null);
// Complete add action in same tick after event completion.
this.plugin.getServer().getScheduler().runTask(this.plugin, () -> player.getInventory().addItem(clone));
}
@Keep
@EventHandler(priority = EventPriority.LOWEST)
private void onInventoryDrag(@NotNull final InventoryDragEvent event) {
if (handleInventoryInteract(event)) {
return;
}
InventoryView view = event.getView();
int topSize = view.getTopInventory().getSize();
// Get bottom inventory active slots as player inventory slots.
Set<Integer> slots = event.getRawSlots().stream()
.filter(slot -> slot >= topSize)
.map(slot -> convertToPlayerSlot(view, slot)).collect(Collectors.toSet());
int overlapLosses = 0;
// Count overlapping slots.
for (Map.Entry<Integer, ItemStack> newItem : event.getNewItems().entrySet()) {
int rawSlot = newItem.getKey();
// Skip bottom inventory slots.
if (rawSlot >= topSize) {
continue;
}
int convertedSlot = convertToPlayerSlot(view, rawSlot);
if (slots.contains(convertedSlot)) {
overlapLosses += getCountDiff(view.getItem(rawSlot), newItem.getValue());
}
}
// Allow no overlap to proceed as usual.
if (overlapLosses < 1) {
return;
}
ItemStack cursor = event.getCursor();
if (cursor != null) {
cursor.setAmount(cursor.getAmount() + overlapLosses);
} else {
cursor = event.getOldCursor().clone();
cursor.setAmount(overlapLosses);
}
event.setCursor(cursor);
}
private int getCountDiff(@Nullable ItemStack original, @NotNull ItemStack result) {
if (original == null || original.getType() != result.getType()) {
return result.getAmount();
}
return result.getAmount() - original.getAmount();
}
/**
* Handle common InventoryInteractEvent functions.
*
* @param event the InventoryInteractEvent
* @return true unless the top inventory is the holder's own inventory
*/
private boolean handleInventoryInteract(@NotNull final InventoryInteractEvent event) {
HumanEntity entity = event.getWhoClicked();
// Un-cancel spectator interactions.
if (Permissions.SPECTATE_CLICK.hasPermission(entity) && entity.getGameMode() == GameMode.SPECTATOR) {
event.setCancelled(false);
}
if (event.isCancelled()) {
return true;
}
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;
} 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);
}
return true;
}
}
private static int convertToPlayerSlot(InventoryView view, int rawSlot) {
int topSize = view.getTopInventory().getSize();
if (topSize <= rawSlot) {
// Slot is not inside special inventory, use Bukkit logic.
return view.convertSlot(rawSlot);
}
// Main inventory, slots 0-26 -> 9-35
if (rawSlot < 27) {
return rawSlot + 9;
}
// Hotbar, slots 27-35 -> 0-8
if (rawSlot < 36) {
return rawSlot - 27;
}
// Armor, slots 36-39 -> 39-36
if (rawSlot < 40) {
return 36 + (39 - rawSlot);
}
// Off-hand
if (rawSlot == 40) {
return 40;
}
// Drop slots, "out of inventory"
return -1;
}
}
@@ -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))
);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,26 +16,34 @@
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.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;
import com.lishid.openinv.commands.ContainerSettingCommand;
import com.lishid.openinv.commands.OpenInvCommand;
import com.lishid.openinv.commands.SearchContainerCommand;
import com.lishid.openinv.commands.SearchEnchantCommand;
import com.lishid.openinv.commands.SearchInvCommand;
import com.lishid.openinv.event.OpenEvents;
import com.lishid.openinv.commands.SearchCommand;
import com.lishid.openinv.internal.IAnySilentContainer;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.listeners.InventoryListener;
import com.lishid.openinv.listeners.PlayerListener;
import com.lishid.openinv.listeners.PluginListener;
import com.lishid.openinv.util.Cache;
import com.lishid.openinv.util.ConfigUpdater;
import com.lishid.openinv.util.InternalAccessor;
import com.lishid.openinv.util.LangMigrator;
import com.lishid.openinv.util.LanguageManager;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.StringMetric;
import com.lishid.openinv.util.lang.LanguageManager;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import net.md_5.bungee.api.ChatMessageType;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
@@ -46,26 +54,13 @@ import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.profile.PlayerProfile;
import org.bukkit.scheduler.BukkitRunnable;
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.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
*
@@ -73,134 +68,163 @@ import java.util.stream.Stream;
*/
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 final Map<String, ISpecialPlayerInventory> inventories = new HashMap<>();
private final Map<String, ISpecialEnderChest> enderChests = new HashMap<>();
private final Multimap<String, Class<? extends Plugin>> pluginUsage = HashMultimap.create();
private final Cache<String, Player> playerCache = new Cache<>(300000L,
value -> {
String key = OpenInv.this.getPlayerID(value);
return OpenInv.this.inventories.containsKey(key)
&& OpenInv.this.inventories.get(key).isInUse()
|| OpenInv.this.enderChests.containsKey(key)
&& OpenInv.this.enderChests.get(key).isInUse()
|| OpenInv.this.pluginUsage.containsKey(key);
},
value -> {
String key = OpenInv.this.getPlayerID(value);
// Check if inventory is stored, and if it is, remove it and eject all viewers
if (OpenInv.this.inventories.containsKey(key)) {
Inventory inv = OpenInv.this.inventories.remove(key).getBukkitInventory();
List<HumanEntity> viewers = new ArrayList<>(inv.getViewers());
for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) {
entity.closeInventory();
}
}
// Check if ender chest is stored, and if it is, remove it and eject all viewers
if (OpenInv.this.enderChests.containsKey(key)) {
Inventory inv = OpenInv.this.enderChests.remove(key).getBukkitInventory();
List<HumanEntity> viewers = new ArrayList<>(inv.getViewers());
for (HumanEntity entity : viewers.toArray(new HumanEntity[0])) {
entity.closeInventory();
}
}
if (!OpenInv.this.disableSaving() && !value.isOnline()) {
value.saveData();
}
});
private InternalAccessor accessor;
private LanguageManager languageManager;
private boolean isSpigot = false;
private OfflineHandler offlineHandler;
@Override
public void reloadConfig() {
super.reloadConfig();
languageManager.reload();
this.offlineHandler = disableOfflineAccess() ? OfflineHandler.REMOVE_AND_CLOSE : OfflineHandler.REQUIRE_PERMISSIONS;
if (this.accessor != null && this.accessor.isSupported()) {
this.accessor.reload(this.getConfig());
}
}
/**
* Evicts all viewers lacking cross-world permissions from a Player's inventory.
*
* @param player the Player
*/
public void changeWorld(final Player player) {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!isSpigot || !this.accessor.isSupported()) {
this.sendVersionError(sender::sendMessage);
return true;
}
return false;
}
String key = this.getPlayerID(player);
@Override
public void onDisable() {
if (this.disableSaving()) {
// Check if the player is cached. If not, neither of their inventories is open.
if (!this.playerCache.containsKey(key)) {
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();
});
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(Player player, ISpecialInventory inventory) {
List<HumanEntity> viewers = new ArrayList<>(inventory.getBukkitInventory().getViewers());
for (HumanEntity human : viewers) {
// If player has permission or is in the same world, allow continued access
// Just in case, also allow null worlds.
if (Permissions.CROSSWORLD.hasPermission(human) || Objects.equals(human.getWorld(), player.getWorld())) {
continue;
}
human.closeInventory();
}
}
/**
* Convert a raw slot number into a player inventory slot number.
*
* <p>Note that this method is specifically for converting an ISpecialPlayerInventory slot number into a regular
* player inventory slot number.
*
* @param view the open inventory view
* @param rawSlot the raw slot in the view
* @return the converted slot number
*/
public int convertToPlayerSlot(InventoryView view, int rawSlot) {
return this.accessor.getPlayerDataManager().convertToPlayerSlot(view, rawSlot);
}
@Override
public void onEnable() {
// Save default configuration if not present.
this.saveDefaultConfig();
// Migrate locale files to a subfolder.
Path dataFolder = getDataFolder().toPath();
new LangMigrator(dataFolder, dataFolder.resolve("locale"), getLogger()).migrate();
this.languageManager = new LanguageManager(this, "en");
this.accessor = new InternalAccessor(getLogger(), languageManager);
try {
Class.forName("org.bukkit.entity.Player$Spigot");
isSpigot = true;
} catch (ClassNotFoundException e) {
isSpigot = false;
}
// Version check
if (isSpigot && this.accessor.isSupported()) {
reloadConfig();
// Update existing configuration. May require internal access.
new ConfigUpdater(this).checkForUpdates();
// 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 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");
} else {
this.sendVersionError(this.getLogger()::warning);
}
public boolean disableSaving() {
return this.getConfig().getBoolean("settings.disable-saving", false);
}
private void setCommandExecutor(@NotNull CommandExecutor executor, String @NotNull ... commands) {
for (String commandName : commands) {
PluginCommand command = this.getCommand(commandName);
if (command != null) {
command.setExecutor(executor);
}
}
@NotNull
@Override
public IAnySilentContainer getAnySilentContainer() {
return this.accessor.getAnySilentContainer();
}
private void sendVersionError(@NotNull Consumer<String> messageMethod) {
if (!accessor.isSupported()) {
messageMethod.accept("Your server version (" + accessor.getVersion() + ") is not supported.");
messageMethod.accept("Please download the correct version of OpenInv here: " + accessor.getReleasesLink());
@Override
public boolean getPlayerAnyChestStatus(@NotNull final OfflinePlayer player) {
boolean defaultState = false;
// We check this property late so users can use jars that were remapped by Paper already.
if (Boolean.getBoolean("paper.disable-plugin-rewriting")) {
messageMethod.accept("OpenInv uses Spigot-mapped internals, but you have disabled plugin rewriting in Paper!");
messageMethod.accept("Please set system property 'paper.disable-plugin-rewriting' to false.");
if (player.isOnline()) {
Player onlinePlayer = player.getPlayer();
if (onlinePlayer != null) {
defaultState = Permissions.ANY_DEFAULT.hasPermission(onlinePlayer);
}
}
if (!isSpigot) {
messageMethod.accept("OpenInv requires that you use Spigot or a Spigot fork. Per the 1.14 update thread");
messageMethod.accept("(https://www.spigotmc.org/threads/369724/ \"A Note on CraftBukkit\"), if you are");
messageMethod.accept("encountering an inconsistency with vanilla that prevents you from using Spigot,");
messageMethod.accept("that is considered a Spigot bug and should be reported as such.");
return this.getConfig().getBoolean("toggles.any-chest." + this.getPlayerID(player), defaultState);
}
@Override
public boolean getPlayerSilentChestStatus(@NotNull final OfflinePlayer offline) {
boolean defaultState = false;
if (offline.isOnline()) {
Player onlinePlayer = offline.getPlayer();
if (onlinePlayer != null) {
defaultState = Permissions.SILENT_DEFAULT.hasPermission(onlinePlayer);
}
}
return this.getConfig().getBoolean("toggles.silent-chest." + this.getPlayerID(offline), defaultState);
}
@NotNull
@Override
public ISpecialEnderChest getSpecialEnderChest(@NotNull final Player player, final boolean online)
throws InstantiationException {
String id = this.getPlayerID(player);
if (this.enderChests.containsKey(id)) {
return this.enderChests.get(id);
}
ISpecialEnderChest inv = this.accessor.newSpecialEnderChest(player, online);
this.enderChests.put(id, inv);
this.playerCache.put(id, player);
return inv;
}
@NotNull
@Override
public ISpecialPlayerInventory getSpecialInventory(@NotNull final Player player, final boolean online)
throws InstantiationException {
String id = this.getPlayerID(player);
if (this.inventories.containsKey(id)) {
return this.inventories.get(id);
}
ISpecialPlayerInventory inv = this.accessor.newSpecialPlayerInventory(player, online);
this.inventories.put(id, inv);
this.playerCache.put(id, player);
return inv;
}
@Override
@@ -208,102 +232,21 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
return this.accessor != null && this.accessor.isSupported();
}
@Override
public boolean disableSaving() {
return this.getConfig().getBoolean("settings.disable-saving", false);
}
@Override
public boolean disableOfflineAccess() {
return this.getConfig().getBoolean("settings.disable-offline-access", false);
}
@Override
public boolean noArgsOpensSelf() {
return this.getConfig().getBoolean("settings.command.open.no-args-opens-self", false);
}
@Override
public @NotNull IAnySilentContainer getAnySilentContainer() {
return this.accessor.getAnySilentContainer();
}
@Override
public boolean getAnyContainerStatus(@NotNull final OfflinePlayer offline) {
return this.getConfig().getBoolean("toggles.any-chest." + offline.getUniqueId(), false);
}
@Override
public void setAnyContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) {
this.getConfig().set("toggles.any-chest." + offline.getUniqueId(), status);
this.saveConfig();
}
@Override
public boolean getSilentContainerStatus(@NotNull final OfflinePlayer offline) {
return this.getConfig().getBoolean("toggles.silent-chest." + offline.getUniqueId(), false);
}
@Override
public void setSilentContainerStatus(@NotNull final OfflinePlayer offline, final boolean status) {
this.getConfig().set("toggles.silent-chest." + offline.getUniqueId(), status);
this.saveConfig();
}
@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;
}
@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;
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
return this.accessor.getPlayerDataManager().openInventory(player, inventory);
}
@Override
public boolean isPlayerLoaded(@NotNull UUID playerUuid) {
return this.inventories.containsKey(playerUuid) || this.enderChests.containsKey(playerUuid);
}
@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();
String key = this.getPlayerID(offline);
if (this.playerCache.containsKey(key)) {
return this.playerCache.get(key);
}
Player player = offline.getPlayer();
if (player != null) {
this.playerCache.put(key, player);
return player;
}
if (disableOfflineAccess() || !this.isSupportedVersion()) {
if (!this.isSupportedVersion()) {
return null;
}
@@ -317,207 +260,222 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
try {
player = future.get();
} catch (InterruptedException | ExecutionException e) {
getLogger().log(Level.WARNING, e.getMessage(), e);
e.printStackTrace();
return null;
}
if (player != null) {
this.playerCache.put(key, player);
}
return player;
}
@Override
public @Nullable OfflinePlayer matchPlayer(@NotNull String name) {
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
return this.accessor.getPlayerDataManager().openInventory(player, inventory);
}
// 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"));
public void sendMessage(@NotNull CommandSender sender, @NotNull String key) {
String message = this.languageManager.getValue(key, getLocale(sender));
if (message != null && !message.isEmpty()) {
sender.sendMessage(message);
}
}
public void sendMessage(@NotNull CommandSender sender, @NotNull String key, String... replacements) {
String message = this.languageManager.getValue(key, getLocale(sender), replacements);
if (message != null && !message.isEmpty()) {
sender.sendMessage(message);
}
}
public void sendSystemMessage(@NotNull Player player, @NotNull String key) {
this.sendSystemMessage(player, key, new String[0]);
}
public void sendSystemMessage(@NotNull Player player, @NotNull String key, String... replacements) {
String message = this.languageManager.getValue(key, getLocale(player));
if (message == null) {
return;
}
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
int newline = message.indexOf('\n');
if (newline != -1) {
// No newlines in action bar chat.
message = message.substring(0, newline);
}
// Exact online match first.
player = Bukkit.getServer().getPlayerExact(name);
if (player != null) {
return player;
if (message.isEmpty()) {
return;
}
// 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;
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, TextComponent.fromLegacyText(message));
}
public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key) {
return this.languageManager.getValue(key, getLocale(sender));
}
public @Nullable String getLocalizedMessage(@NotNull CommandSender sender, @NotNull String key, String... replacements) {
return this.languageManager.getValue(key, getLocale(sender), replacements);
}
private @NotNull String getLocale(@NotNull CommandSender sender) {
if (sender instanceof Player) {
return ((Player) sender).getLocale();
} else {
return this.getConfig().getString("settings.locale", "en_us");
}
}
// Exact offline match second - ensure offline access works when matchable users are online.
player = Bukkit.getServer().getOfflinePlayer(name);
public int getSearchPollsPerTick() {
return this.getConfig().getInt("settings.search.polls-per-tick", 5);
}
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;
public int getSearchResultsMax() {
return this.getConfig().getInt("settings.search.max-results", 40);
}
@Override
public void unload(@NotNull final OfflinePlayer offline) {
setPlayerOffline(offline, OfflineHandler.REMOVE_AND_CLOSE);
}
public void onDisable() {
/**
* 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.disableSaving()) {
return;
}
if (this.enderChests.containsKey(key)) {
kickCrossWorldViewers(player, this.enderChests.get(key));
if (this.isSupportedVersion()) {
this.playerCache.invalidateAll();
}
}
private void kickCrossWorldViewers(@NotNull Player player, @NotNull ISpecialInventory inventory) {
ejectViewers(
inventory,
viewer ->
!Permissions.ACCESS_CROSSWORLD.hasPermission(viewer)
&& !Objects.equals(viewer.getWorld(), player.getWorld()));
@Override
public void onEnable() {
// Save default configuration if not present.
this.saveDefaultConfig();
// Get plugin manager
PluginManager pm = this.getServer().getPluginManager();
this.accessor = new InternalAccessor(this);
this.languageManager = new LanguageManager(this, "en_us");
// Version check
if (this.accessor.isSupported()) {
// Update existing configuration. May require internal access.
new ConfigUpdater(this).checkForUpdates();
// Register listeners
pm.registerEvents(new PlayerListener(this), this);
pm.registerEvents(new PluginListener(this), this);
pm.registerEvents(new InventoryListener(this), this);
// Register commands to their executors
this.setCommandExecutor(new OpenInvCommand(this), "openinv", "openender");
this.setCommandExecutor(new SearchCommand(this), "search");
this.setCommandExecutor(new ContainerSettingCommand(this), "silentcontainer", "anycontainer");
} else {
this.sendVersionError(this.getLogger()::warning);
}
}
private void sendVersionError(Consumer<String> messageMethod) {
messageMethod.accept("Your server version (" + this.accessor.getVersion() + ") is not supported.");
messageMethod.accept("Please obtain an appropriate version here: " + accessor.getReleasesLink());
}
private void setCommandExecutor(CommandExecutor executor, String... commands) {
for (String commandName : commands) {
PluginCommand command = this.getCommand(commandName);
if (command != null) {
command.setExecutor(executor);
}
}
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!this.accessor.isSupported()) {
this.sendVersionError(sender::sendMessage);
return true;
}
return false;
}
public void releaseAllPlayers(final Plugin plugin) {
Iterator<Map.Entry<String, Class<? extends Plugin>>> iterator = this.pluginUsage.entries().iterator();
if (!iterator.hasNext()) {
return;
}
for (Map.Entry<String, Class<? extends Plugin>> entry = iterator.next(); iterator.hasNext(); entry = iterator.next()) {
if (entry.getValue().equals(plugin.getClass())) {
iterator.remove();
}
}
}
@Override
public void releasePlayer(@NotNull final Player player, @NotNull final Plugin plugin) {
String key = this.getPlayerID(player);
if (!this.pluginUsage.containsEntry(key, plugin.getClass())) {
return;
}
this.pluginUsage.remove(key, plugin.getClass());
}
@Override
public void retainPlayer(@NotNull final Player player, @NotNull final Plugin plugin) {
String key = this.getPlayerID(player);
if (this.pluginUsage.containsEntry(key, plugin.getClass())) {
return;
}
this.pluginUsage.put(key, plugin.getClass());
}
@Override
public void setPlayerAnyChestStatus(@NotNull final OfflinePlayer offline, final boolean status) {
this.getConfig().set("toggles.any-chest." + this.getPlayerID(offline), status);
this.saveConfig();
}
/**
* Method for handling a Player going offline.
*
* @param player the Player
* @throws IllegalStateException if the server version is unsupported
*/
void setPlayerOffline(@NotNull Player player) {
setPlayerOffline(player, offlineHandler);
}
public void setPlayerOffline(final Player player) {
private void setPlayerOffline(@NotNull OfflinePlayer player, @NotNull OfflineHandler handler) {
UUID key = player.getUniqueId();
String key = this.getPlayerID(player);
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.
// Check if the player is cached. If not, neither of their inventories is open.
if (!this.playerCache.containsKey(key)) {
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));
}
// Replace stored player with our own version
this.playerCache.put(key, this.accessor.getPlayerDataManager().inject(player));
if (this.inventories.containsKey(key)) {
this.inventories.get(key).setPlayerOffline();
}
// 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();
}
});
if (this.enderChests.containsKey(key)) {
this.enderChests.get(key).setPlayerOffline();
}
}
/**
@@ -526,83 +484,78 @@ public class OpenInv extends JavaPlugin implements IOpenInv {
* @param player the Player
* @throws IllegalStateException if the server version is unsupported
*/
void setPlayerOnline(@NotNull Player player) {
setPlayerOnline(inventories, player);
setPlayerOnline(enderChests, player);
public void setPlayerOnline(final Player player) {
if (player.hasPlayedBefore()) {
String key = this.getPlayerID(player);
// Check if the player is cached. If not, neither of their inventories is open.
if (!this.playerCache.containsKey(key)) {
return;
}
// New player may have a name that already points to someone else in lookup cache.
String name = player.getName();
this.offlineLookUpCache.invalidate(name);
this.playerCache.put(key, player);
// If no offline matches are mapped, don't hit scheduler.
if (this.offlineLookUpCache.size() == 0) {
return;
if (this.inventories.containsKey(key)) {
this.inventories.get(key).setPlayerOnline(player);
new BukkitRunnable() {
@Override
public void run() {
if (player.isOnline()) {
player.updateInventory();
}
}
}.runTask(this);
}
// 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.
if (this.enderChests.containsKey(key)) {
this.enderChests.get(key).setPlayerOnline(player);
}
}
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())));
@Override
public void setPlayerSilentChestStatus(@NotNull final OfflinePlayer offline, final boolean status) {
this.getConfig().set("toggles.silent-chest." + this.getPlayerID(offline), status);
this.saveConfig();
}
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.
/**
* Displays all applicable help for OpenInv commands.
*
* @param player the Player to help
*/
public void showHelp(final Player player) {
// Get registered commands
for (String commandName : this.getDescription().getCommands().keySet()) {
PluginCommand command = this.getCommand(commandName);
// Ensure command is successfully registered and player can use it
if (command == null || !command.testPermissionSilent(player)) {
continue;
}
if (predicate.test(viewer)) {
viewer.closeInventory();
// Send usage
player.sendMessage(command.getUsage().replace("<command>", commandName));
List<String> aliases = command.getAliases();
if (aliases.isEmpty()) {
continue;
}
// Assemble alias list
StringBuilder aliasBuilder = new StringBuilder(" (aliases: ");
for (String alias : aliases) {
aliasBuilder.append(alias).append(", ");
}
aliasBuilder.delete(aliasBuilder.length() - 2, aliasBuilder.length()).append(')');
// Send all aliases
player.sendMessage(aliasBuilder.toString());
}
}
@Override
public void unload(@NotNull final OfflinePlayer offline) {
this.playerCache.invalidate(this.getPlayerID(offline));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,10 +16,12 @@
package com.lishid.openinv.commands;
import com.lishid.openinv.IOpenInv;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
import com.lishid.openinv.util.lang.Replacement;
import java.util.Collections;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
@@ -27,35 +29,28 @@ import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
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;
public class ContainerSettingCommand implements TabExecutor {
private final @NotNull IOpenInv plugin;
private final @NotNull LanguageManager lang;
private final OpenInv plugin;
public ContainerSettingCommand(@NotNull IOpenInv plugin, @NotNull LanguageManager lang) {
public ContainerSettingCommand(final OpenInv plugin) {
this.plugin = plugin;
this.lang = lang;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player player)) {
lang.sendMessage(sender, "messages.error.consoleUnsupported");
if (!(sender instanceof Player)) {
plugin.sendMessage(sender, "messages.error.consoleUnsupported");
return true;
}
Player player = (Player) sender;
boolean any = command.getName().startsWith("any");
Predicate<Player> getSetting = any ? plugin::getAnyContainerStatus : plugin::getSilentContainerStatus;
BiConsumer<OfflinePlayer, Boolean> setSetting = any ? plugin::setAnyContainerStatus : plugin::setSilentContainerStatus;
Predicate<Player> getSetting = any ? plugin::getPlayerAnyChestStatus : plugin::getPlayerSilentChestStatus;
BiConsumer<OfflinePlayer, Boolean> setSetting = any ? plugin::setPlayerAnyChestStatus : plugin::setPlayerSilentChestStatus;
if (args.length > 0) {
args[0] = args[0].toLowerCase(Locale.ENGLISH);
args[0] = args[0].toLowerCase();
if (args[0].equals("on")) {
setSetting.accept(player, true);
@@ -70,16 +65,12 @@ public class ContainerSettingCommand implements TabExecutor {
setSetting.accept(player, !getSetting.test(player));
}
String onOff = lang.getLocalizedMessage(player, getSetting.test(player) ? "messages.info.on" : "messages.info.off");
String onOff = plugin.getLocalizedMessage(player, getSetting.test(player) ? "messages.info.on" : "messages.info.off");
if (onOff == null) {
onOff = String.valueOf(getSetting.test(player));
}
lang.sendMessage(
sender,
"messages.info.settingState",
new Replacement("%setting%", any ? "AnyContainer" : "SilentContainer"),
new Replacement("%state%", onOff));
plugin.sendMessage(sender, "messages.info.settingState","%setting%", any ? "AnyContainer" : "SilentContainer", "%state%", onOff);
return true;
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -20,66 +20,55 @@ import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
import com.lishid.openinv.util.lang.Replacement;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.bukkit.OfflinePlayer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.PluginCommand;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.StringJoiner;
import java.util.logging.Level;
public class OpenInvCommand implements TabExecutor {
private final @NotNull OpenInv plugin;
private final @NotNull LanguageManager lang;
private final OpenInv plugin;
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(final OpenInv plugin) {
this.plugin = plugin;
this.lang = lang;
}
@Override
public boolean onCommand(@NotNull final CommandSender sender, @NotNull final Command command, @NotNull final String label, @NotNull final String[] args) {
boolean openInv = command.getName().equals("openinv");
if (openInv && args.length > 0 && (args[0].equalsIgnoreCase("help") || args[0].equals("?"))) {
this.showHelp(sender);
if (!(sender instanceof Player)) {
plugin.sendMessage(sender, "messages.error.consoleUnsupported");
return true;
}
if (!(sender instanceof Player player)) {
lang.sendMessage(sender, "messages.error.consoleUnsupported");
if (args.length > 0 && (args[0].equalsIgnoreCase("help") || args[0].equals("?"))) {
this.plugin.showHelp((Player) sender);
return true;
}
String noArgValue;
if (plugin.noArgsOpensSelf()) {
noArgValue = player.getUniqueId().toString();
} else {
// History management
noArgValue = (openInv ? this.openInvHistory : this.openEnderHistory).get(player);
final Player player = (Player) sender;
final boolean openinv = command.getName().equals("openinv");
if (noArgValue == null || noArgValue.isEmpty()) {
noArgValue = player.getUniqueId().toString();
(openInv ? this.openInvHistory : this.openEnderHistory).put(player, noArgValue);
}
// History management
String history = (openinv ? this.openInvHistory : this.openEnderHistory).get(player);
if (history == null || history.isEmpty()) {
history = player.getName();
(openinv ? this.openInvHistory : this.openEnderHistory).put(player, history);
}
final String name;
// Read from history if target is not named
if (args.length < 1) {
name = noArgValue;
name = history;
} else {
name = args[0];
}
@@ -89,8 +78,8 @@ public class OpenInvCommand implements TabExecutor {
public void run() {
final OfflinePlayer offlinePlayer = OpenInvCommand.this.plugin.matchPlayer(name);
if (offlinePlayer == null || (!offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline())) {
lang.sendMessage(player, "messages.error.invalidPlayer");
if (offlinePlayer == null || !offlinePlayer.hasPlayedBefore() && !offlinePlayer.isOnline()) {
plugin.sendMessage(player, "messages.error.invalidPlayer");
return;
}
@@ -100,7 +89,7 @@ public class OpenInvCommand implements TabExecutor {
if (!player.isOnline()) {
return;
}
OpenInvCommand.this.openInventory(player, offlinePlayer, openInv);
OpenInvCommand.this.openInventory(player, offlinePlayer, openinv);
}
}.runTask(OpenInvCommand.this.plugin);
@@ -110,111 +99,73 @@ public class OpenInvCommand implements TabExecutor {
return true;
}
private void showHelp(final CommandSender sender) {
// Get registered commands
for (String commandName : plugin.getDescription().getCommands().keySet()) {
PluginCommand command = plugin.getCommand(commandName);
// Ensure command is successfully registered and sender can use it
if (command == null || !command.testPermissionSilent(sender)) {
continue;
}
// Send usage
sender.sendMessage(command.getUsage().replace("<command>", commandName));
List<String> aliases = command.getAliases();
if (!aliases.isEmpty()) {
// Assemble alias list
StringJoiner aliasJoiner = new StringJoiner(", ", " (aliases: ", ")");
for (String alias : aliases) {
aliasJoiner.add(alias);
}
// Send all aliases
sender.sendMessage(aliasJoiner.toString());
}
}
}
private void openInventory(final Player player, final OfflinePlayer target, boolean openinv) {
Player onlineTarget;
boolean online = target.isOnline();
if (!online) {
if (!plugin.disableOfflineAccess() && Permissions.ACCESS_OFFLINE.hasPermission(player)) {
if (Permissions.OPENOFFLINE.hasPermission(player)) {
// Try loading the player's data
onlineTarget = this.plugin.loadPlayer(target);
} else {
lang.sendMessage(player, "messages.error.permissionPlayerOffline");
plugin.sendMessage(player, "messages.error.permissionPlayerOffline");
return;
}
} else {
if (Permissions.ACCESS_ONLINE.hasPermission(player)) {
if (Permissions.OPENONLINE.hasPermission(player)) {
onlineTarget = target.getPlayer();
} else {
lang.sendMessage(player, "messages.error.permissionPlayerOnline");
plugin.sendMessage(player, "messages.error.permissionPlayerOnline");
return;
}
}
if (onlineTarget == null) {
lang.sendMessage(player, "messages.error.invalidPlayer");
plugin.sendMessage(player, "messages.error.invalidPlayer");
return;
}
// Permissions checks
if (onlineTarget.equals(player)) {
// Permission for opening own inventory.
if (!(openinv ? Permissions.INVENTORY_OPEN_SELF : Permissions.ENDERCHEST_OPEN_SELF).hasPermission(player)) {
lang.sendMessage(player, "messages.error.permissionOpenSelf");
// Inventory: Additional permission required to open own inventory
if (openinv && !Permissions.OPENSELF.hasPermission(player)) {
plugin.sendMessage(player, "messages.error.permissionOpenSelf");
return;
}
} else {
// Permission for opening others' inventories.
if (!(openinv ? Permissions.INVENTORY_OPEN_OTHER : Permissions.ENDERCHEST_OPEN_OTHER).hasPermission(player)) {
lang.sendMessage(player, "messages.error.permissionOpenOther");
// Enderchest: Additional permission required to open others' ender chests
if (!openinv && !Permissions.ENDERCHEST_ALL.hasPermission(player)) {
plugin.sendMessage(player, "messages.error.permissionEnderAll");
return;
}
// Protected check
for (int level = 4; level > 0; --level) {
String permission = "openinv.access.level." + level;
if (onlineTarget.hasPermission(permission)
&& !player.hasPermission(permission)) {
lang.sendMessage(
player,
"messages.error.permissionExempt",
new Replacement("%target%", onlineTarget.getDisplayName()));
return;
}
if (!Permissions.OVERRIDE.hasPermission(player)
&& Permissions.EXEMPT.hasPermission(onlineTarget)) {
plugin.sendMessage(player, "messages.error.permissionExempt",
"%target%", onlineTarget.getDisplayName());
return;
}
// Crossworld check
if (!Permissions.ACCESS_CROSSWORLD.hasPermission(player)
if (!Permissions.CROSSWORLD.hasPermission(player)
&& !onlineTarget.getWorld().equals(player.getWorld())) {
lang.sendMessage(
player,
"messages.error.permissionCrossWorld",
new Replacement("%target%", onlineTarget.getDisplayName()));
plugin.sendMessage(player, "messages.error.permissionCrossWorld",
"%target%", onlineTarget.getDisplayName());
return;
}
}
if (!plugin.noArgsOpensSelf()) {
// Record the target
(openinv ? this.openInvHistory : this.openEnderHistory).put(player, target.getUniqueId().toString());
}
// Record the target
(openinv ? this.openInvHistory : this.openEnderHistory).put(player, this.plugin.getPlayerID(target));
// Create the inventory
final ISpecialInventory inv;
try {
inv = openinv ? this.plugin.getSpecialInventory(onlineTarget, online) : this.plugin.getSpecialEnderChest(onlineTarget, online);
} catch (Exception e) {
lang.sendMessage(player, "messages.error.commandException");
plugin.getLogger().log(Level.WARNING, "Unable to create ISpecialInventory", e);
plugin.sendMessage(player, "messages.error.commandException");
e.printStackTrace();
return;
}
@@ -0,0 +1,249 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.commands.search.ChunkSearchOption;
import com.lishid.openinv.commands.search.CompletableOption;
import com.lishid.openinv.commands.search.ItemAmountOption;
import com.lishid.openinv.commands.search.ItemEnchantOption;
import com.lishid.openinv.commands.search.ItemTypeOption;
import com.lishid.openinv.commands.search.PlayerSearchOption;
import com.lishid.openinv.commands.search.PseudoOption;
import com.lishid.openinv.search.Search;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchableItem;
import com.lishid.openinv.search.match.MatchableMeta;
import com.lishid.openinv.util.PseudoJson;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class SearchCommand implements TabExecutor {
private final OpenInv plugin;
private final Collection<String> activeSearches = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Collection<CompletableOption<SearchBucket>> completableSearchBuckets = new HashSet<>();
private final Collection<CompletableOption<MatchableItem>> completableMatchOptions = new HashSet<>();
private final Collection<CompletableOption<MatchableMeta>> completableMatchMetaOptions = new HashSet<>();
public SearchCommand(OpenInv plugin) {
this.plugin = plugin;
this.completableSearchBuckets.add(new ChunkSearchOption());
this.completableSearchBuckets.add(new PlayerSearchOption(plugin));
this.completableMatchOptions.add(new ItemAmountOption());
this.completableMatchOptions.add(new ItemTypeOption());
this.completableMatchMetaOptions.add(new ItemEnchantOption());
}
@Override
public boolean onCommand(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String label,
@NotNull String[] args) {
if (args.length < 2) {
return false;
}
String id = sender instanceof Player ? plugin.getPlayerID((Player) sender) : "CONSOLE";
if (activeSearches.contains(id)) {
plugin.sendMessage(sender, "messages.error.search.activeSearch");
return true;
}
Collection<PseudoJson> arguments = new HashSet<>();
for (String arg : args) {
arg = arg.toLowerCase(Locale.ROOT);
try {
arguments.add(PseudoJson.fromString(arg));
} catch (IllegalArgumentException e) {
plugin.sendMessage(sender, "messages.error.search.invalidSchema", "%value%", arg);
return true;
}
}
Collection<SearchBucket> searchBuckets = getSearchBuckets(sender, arguments);
if (searchBuckets.isEmpty()) {
plugin.sendMessage(sender, "messages.error.search.invalidInventory");
return true;
}
ItemMatcher matcher = getMatcher(sender, arguments);
if (matcher == null) {
plugin.sendMessage(sender, "messages.error.search.invalidMatcher");
return true;
}
activeSearches.add(id);
new Search(matcher, searchBuckets, () -> activeSearches.remove(id)).schedule(sender, plugin);
return true;
}
private @Nullable ItemMatcher getMatcher(@NotNull CommandSender sender, @NotNull Collection<PseudoJson> args) {
List<PseudoOption<MatchableItem>> matchItemOptions = new ArrayList<>();
List<PseudoOption<MatchableMeta>> matchMetaOptions = new ArrayList<>();
for (PseudoJson arg : args) {
addMatchOptions(completableMatchOptions, matchItemOptions, sender, arg);
addMatchOptions(completableMatchMetaOptions, matchMetaOptions, sender, arg);
}
// Must have a match option. Null entries indicate a parsing error.
if (matchItemOptions.isEmpty()
|| matchItemOptions.stream().anyMatch(Objects::isNull)
|| matchMetaOptions.stream().anyMatch(Objects::isNull)) {
return null;
}
return new ItemMatcher(
matchItemOptions.stream().map(PseudoOption::option).toList(),
matchMetaOptions.stream().map(PseudoOption::option).toList());
}
private <T> void addMatchOptions(
@NotNull Collection<CompletableOption<T>> completableOptions,
@NotNull Collection<PseudoOption<T>> options,
@NotNull CommandSender sender,
@NotNull PseudoJson arg) {
for (CompletableOption<T> option : completableOptions) {
if (option.matches(sender, arg)) {
PseudoOption<T> parsed = option.parse(sender, arg);
if (parsed == null) {
options.add(null);
continue;
}
if (option.isUnique()) {
String uniqueId = option.getName();
// Match existing options.
List<PseudoOption<T>> matches = options.stream()
.filter(Objects::nonNull)
.filter(bucket -> bucket.pseudoJson().getIdentifier().equals(uniqueId))
.collect(Collectors.toList());
// Remove all existing options.
options.removeAll(matches);
// Merge with existing options.
for (PseudoOption<T> match : matches) {
PseudoOption<T> newParsed = option.merge(sender, match, parsed);
if (newParsed != null) {
parsed = newParsed;
}
}
}
options.add(parsed);
}
}
}
private Collection<SearchBucket> getSearchBuckets(
@NotNull CommandSender sender,
@NotNull Collection<PseudoJson> args) {
List<PseudoOption<SearchBucket>> buckets = new ArrayList<>();
for (PseudoJson arg : args) {
addMatchOptions(completableSearchBuckets, buckets, sender, arg);
}
return buckets.stream().map(PseudoOption::option).collect(Collectors.toList());
}
@Override
public @Nullable List<String> onTabComplete(
@NotNull CommandSender sender,
@NotNull Command command,
@NotNull String label,
@NotNull String[] args) {
List<PseudoJson> arguments = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i].toLowerCase(Locale.ROOT);
try {
arguments.add(PseudoJson.fromString(arg));
} catch (IllegalArgumentException e) {
if (i == args.length - 1) {
// If last argument cannot be parsed, do not complete.
return List.of();
}
// Otherwise, ignore formatting issues during tab completion.
}
}
// Ensure an argument exists.
if (arguments.isEmpty()) {
arguments.add(new PseudoJson(""));
}
// Remove last indexed argument, it is being completed.
PseudoJson last = arguments.remove(arguments.size() - 1);
// Due to generics, can't just use generic list of options to start with.
Collection<PseudoOption<SearchBucket>> buckets = new HashSet<>();
Collection<PseudoOption<MatchableItem>> items = new HashSet<>();
Collection<PseudoOption<MatchableMeta>> metas = new HashSet<>();
for (PseudoJson arg : arguments) {
addMatchOptions(completableSearchBuckets, buckets, sender, arg);
addMatchOptions(completableMatchOptions, items, sender, arg);
addMatchOptions(completableMatchMetaOptions, metas, sender, arg);
}
Collection<PseudoOption<?>> options = new HashSet<>();
options.addAll(buckets);
options.addAll(items);
options.addAll(metas);
// Suggest all completable options' completions.
return Stream.concat(completableSearchBuckets.stream(),
Stream.concat(completableMatchOptions.stream(),
completableMatchMetaOptions.stream()))
// Don't suggest unique options that are already present.
.filter(option ->
option.isUnique() && options.stream().anyMatch(pseudoOption ->
pseudoOption.pseudoJson().getIdentifier().equals(option.getName())))
.flatMap(option -> option.suggestOptions(sender, last).stream())
.distinct()
.toList();
}
}
@@ -1,145 +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.commands;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
import com.lishid.openinv.util.lang.Replacement;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.BlockState;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.plugin.Plugin;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Command for searching containers in a radius of chunks.
*/
public class SearchContainerCommand implements TabExecutor {
private final @NotNull Plugin plugin;
private final @NotNull LanguageManager lang;
public SearchContainerCommand(@NotNull Plugin plugin, @NotNull LanguageManager lang) {
this.plugin = plugin;
this.lang = lang;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!(sender instanceof Player senderPlayer)) {
lang.sendMessage(sender, "messages.error.consoleUnsupported");
return true;
}
if (args.length < 1) {
// Must supply material
return false;
}
Material material = Material.matchMaterial(args[0]);
if (material == null) {
lang.sendMessage(
sender,
"messages.error.invalidMaterial",
new Replacement("%target%", args[0]));
return false;
}
int radius = 5;
if (args.length > 1) {
try {
radius = Integer.parseInt(args[1]);
} catch (NumberFormatException e) {
// Invalid radius supplied
return false;
}
}
// Clamp radius.
int configMax = plugin.getConfig().getInt("settings.command.searchcontainer.max-radius", 10);
radius = Math.max(0, Math.min(radius, configMax));
World world = senderPlayer.getWorld();
Chunk centerChunk = senderPlayer.getLocation().getChunk();
StringBuilder locations = new StringBuilder();
for (int dX = -radius; dX <= radius; ++dX) {
for (int dZ = -radius; dZ <= radius; ++dZ) {
if (!world.loadChunk(centerChunk.getX() + dX, centerChunk.getZ() + dZ, false)) {
continue;
}
Chunk chunk = world.getChunkAt(centerChunk.getX() + dX, centerChunk.getZ() + dZ);
for (BlockState tileEntity : chunk.getTileEntities()) {
if (!(tileEntity instanceof InventoryHolder holder)) {
continue;
}
if (!holder.getInventory().contains(material)) {
continue;
}
locations.append(holder.getInventory().getType().name().toLowerCase(Locale.ENGLISH)).append(" (")
.append(tileEntity.getX()).append(',').append(tileEntity.getY()).append(',')
.append(tileEntity.getZ()).append("), ");
}
}
}
// Matches found, delete trailing comma and space
if (!locations.isEmpty()) {
locations.delete(locations.length() - 2, locations.length());
} else {
lang.sendMessage(
sender,
"messages.info.container.noMatches",
new Replacement("%target%", material.name()));
return true;
}
lang.sendMessage(
sender,
"messages.info.container.matches",
new Replacement("%target%", material.name()),
new Replacement("%detail%", locations.toString()));
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] args) {
if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
return Collections.emptyList();
}
String argument = args[args.length - 1];
if (args.length == 1) {
return TabCompleter.completeEnum(argument, Material.class);
} else {
return TabCompleter.completeInteger(argument);
}
}
}
@@ -1,172 +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.commands;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
import com.lishid.openinv.util.lang.Replacement;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
/**
* Command adding the ability to search online players' inventories for enchantments of a specific
* type at or above the level specified.
*
* @author Jikoo
*/
public class SearchEnchantCommand implements TabExecutor {
private final LanguageManager lang;
public SearchEnchantCommand(LanguageManager lang) {
this.lang = lang;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length == 0) {
return false;
}
Enchantment enchant = null;
int level = 0;
for (String argument : args) {
try {
level = Integer.parseInt(argument);
continue;
} catch (NumberFormatException ignored) {
// Not a level being specified.
}
argument = argument.toLowerCase(Locale.ENGLISH);
NamespacedKey key = NamespacedKey.fromString(argument);
if (key == null) {
continue;
}
Enchantment localEnchant = Registry.ENCHANTMENT.get(key);
if (localEnchant != null) {
enchant = localEnchant;
}
}
// Arguments not set correctly
if (level == 0 && enchant == null) {
return false;
}
StringBuilder players = new StringBuilder();
for (Player player : Bukkit.getServer().getOnlinePlayers()) {
boolean flagInventory = containsEnchantment(player.getInventory(), enchant, level);
boolean flagEnder = containsEnchantment(player.getEnderChest(), enchant, level);
// No matches, continue
if (!flagInventory && !flagEnder) {
continue;
}
// Matches, append details
players.append(player.getName()).append(" (");
if (flagInventory) {
players.append("inv");
}
if (flagEnder) {
if (flagInventory) {
players.append(',');
}
players.append("ender");
}
players.append("), ");
}
if (!players.isEmpty()) {
// Matches found, delete trailing comma and space
players.delete(players.length() - 2, players.length());
} else {
lang.sendMessage(
sender,
"messages.info.player.noMatches",
new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level));
return true;
}
lang.sendMessage(
sender,
"messages.info.player.matches",
new Replacement("%target%", (enchant != null ? enchant.getKey().toString() : "") + " >= " + level),
new Replacement("%detail%", players.toString()));
return true;
}
private boolean containsEnchantment(Inventory inventory, @Nullable Enchantment enchant, int minLevel) {
for (ItemStack item : inventory.getContents()) {
if (item == null || item.getType() == Material.AIR) {
continue;
}
if (enchant != null) {
if (item.containsEnchantment(enchant) && item.getEnchantmentLevel(enchant) >= minLevel) {
return true;
}
} else {
if (!item.hasItemMeta()) {
continue;
}
ItemMeta meta = item.getItemMeta();
if (meta == null || !meta.hasEnchants()) {
continue;
}
for (int enchLevel : meta.getEnchants().values()) {
if (enchLevel >= minLevel) {
return true;
}
}
}
}
return false;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (!command.testPermissionSilent(sender) || args.length < 1 || args.length > 2) {
return Collections.emptyList();
}
if (args.length == 1) {
return TabCompleter.completeObject(args[0], enchantment -> enchantment.getKey().toString(), Registry.ENCHANTMENT.stream().toArray(Enchantment[]::new));
} else {
return TabCompleter.completeInteger(args[1]);
}
}
}
@@ -1,123 +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.commands;
import com.lishid.openinv.util.TabCompleter;
import com.lishid.openinv.util.lang.LanguageManager;
import com.lishid.openinv.util.lang.Replacement;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabExecutor;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.List;
public class SearchInvCommand implements TabExecutor {
private final LanguageManager lang;
public SearchInvCommand(LanguageManager lang) {
this.lang = lang;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
Material material = null;
if (args.length >= 1) {
material = Material.matchMaterial(args[0]);
}
if (material == null) {
lang.sendMessage(
sender,
"messages.error.invalidMaterial",
new Replacement("%target%", args.length > 0 ? args[0] : "null"));
return false;
}
int count = 1;
if (args.length >= 2) {
try {
count = Integer.parseInt(args[1]);
} catch (NumberFormatException ex) {
lang.sendMessage(
sender,
"messages.error.invalidNumber",
new Replacement("%target%", args[1]));
return false;
}
}
StringBuilder players = new StringBuilder();
boolean searchInv = command.getName().equals("searchinv");
for (Player player : Bukkit.getServer().getOnlinePlayers()) {
Inventory inventory = searchInv ? player.getInventory() : player.getEnderChest();
int total = 0;
for (ItemStack itemStack : inventory.getContents()) {
if (itemStack != null && itemStack.getType() == material) {
total += itemStack.getAmount();
if (total >= count) {
players.append(player.getName()).append(", ");
break;
}
}
}
}
// Matches found, delete trailing comma and space
if (!players.isEmpty()) {
players.delete(players.length() - 2, players.length());
} else {
lang.sendMessage(
sender,
"messages.info.player.noMatches",
new Replacement("%target%", material.name()));
return true;
}
lang.sendMessage(
sender,
"messages.info.player.matches",
new Replacement("%target%", material.name()),
new Replacement("%detail%", players.toString()));
return true;
}
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
if (args.length < 1 || args.length > 2 || !command.testPermissionSilent(sender)) {
return Collections.emptyList();
}
String argument = args[args.length - 1];
if (args.length == 1) {
return TabCompleter.completeEnum(argument, Material.class);
} else {
return TabCompleter.completeInteger(argument);
}
}
}
@@ -0,0 +1,125 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.search.bucket.ChunkBucket;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.PseudoJson;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ChunkSearchOption implements CompletableOption<SearchBucket> {
@Override
public @NotNull String getName() {
return "chunk";
}
@Override
public boolean matches(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
return CompletableOption.super.matches(sender, pseudoJson)
&& Permissions.SEARCH_CHUNKS_LOADED.hasPermission(sender);
}
@Override
public @Nullable PseudoOption<SearchBucket> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
World world = null;
Integer chunkX = null;
Integer chunkZ = null;
int chunkRadius = 5;
boolean chunkLoad = false;
Map<String, String> mappings = pseudoJson.getMappings();
if (!(sender instanceof Player senderPlayer)) {
if (mappings.size() < 3) {
// Console must specify world, x center, z center.
return null;
}
} else {
world = senderPlayer.getWorld();
Chunk senderChunk = senderPlayer.getLocation().getChunk();
chunkX = senderChunk.getX();
chunkZ = senderChunk.getZ();
}
for (Map.Entry<String, String> entry : mappings.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (key.indexOf('w') >= 0) {
world = null;
// Name should always be lower case by this point. Ignore case when selecting world.
for (World loaded : Bukkit.getWorlds()) {
if (loaded.getName().equalsIgnoreCase(value)) {
world = loaded;
break;
}
}
} else if (key.indexOf('x') >= 0) {
try {
chunkX = Integer.parseInt(value);
} catch (NumberFormatException e) {
chunkX = null;
}
} else if (key.indexOf('z') >= 0) {
try {
chunkZ = Integer.parseInt(value);
} catch (NumberFormatException e) {
chunkZ = null;
}
} else if (key.indexOf('r') >= 0) {
// R is for radius. Needs to be checked later so that "world" doesn't result in a false positive.
try {
chunkRadius = Integer.parseInt(value);
} catch (NumberFormatException e) {
// Fall through to default radius.
}
} else if (key.indexOf('l') >= 0) {
// L is for loaded. Needs to be checked later so that "world" doesn't result in a false positive.
chunkLoad = Permissions.SEARCH_CHUNKS_UNLOADED.hasPermission(sender) && Boolean.parseBoolean(value);
}
}
if (world == null || chunkX == null || chunkZ == null || chunkRadius < 0) {
// Invalid data provided.
return null;
}
return new PseudoOption<>(pseudoJson, new ChunkBucket(world, chunkX, chunkZ, chunkRadius, chunkLoad));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
// TODO
return Set.of();
}
@Override
public boolean isUnique() {
return false;
}
}
@@ -0,0 +1,110 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.util.PseudoJson;
import java.util.Collection;
import java.util.Locale;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Interface defining behavior for an option.
*
* @param <T> the type of option represented
*/
public interface CompletableOption<T> {
/**
* Get unique identifier representing the option.
*
* @return the unique identifier
*/
@NotNull String getName();
/**
* Check if the <code>CompletableOption</code> matches the given <code>PseudoJson</code>.
*
* @param sender the individual using the option
* @param pseudoJson the pseudo-JSON element
* @return true if the CompletableOption matches
*/
default boolean matches(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
return pseudoJson.getIdentifier().toLowerCase(Locale.ROOT).startsWith(getName());
}
/**
* Parse the option represented from the given <code>PseudoJson</code>.
*
* @param sender the individual using the option
* @param pseudoJson the pseudo-JSON element
* @return the parsed options
*/
@Nullable PseudoOption<T> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson);
/**
* Form a collection of viable permutations to suggest for the given <code>PseudoJson</code>.
*
* @param sender the individual to suggest options for
* @param pseudoJson the parsed pseudo-JSON
* @return the suggested permutations
*/
@NotNull Collection<String> suggestOptions(
@NotNull CommandSender sender,
@NotNull PseudoJson pseudoJson);
/**
* Get whether or not multiple different copies of this <code>CompletableOption</code> are allowed.
*
* @return true if only a single instance is allowed
*/
boolean isUnique();
/**
* Merge similar elements. Used to enforce {@link #isUnique()} despite multiples being provided.
*
* @param sender the individual using the options
* @param first the first parsed option
* @param second the second parsed option
* @return a single element containing a best-effort merge of both
*/
default @Nullable PseudoOption<T> merge(
@NotNull CommandSender sender,
@NotNull PseudoOption<?> first,
@NotNull PseudoOption<?> second) {
PseudoJson merge = new PseudoJson(first.pseudoJson().getIdentifier());
// Use mappings of first for our base.
first.pseudoJson().getMappings().forEach(merge::put);
// Overwrite with second.
second.pseudoJson().getMappings().forEach(merge::put);
// Parse merged options.
PseudoOption<T> parsed = parse(sender, merge);
if (parsed != null) {
return parsed;
}
// On parsing failure, try second instead.
merge = new PseudoJson(second.pseudoJson().getIdentifier(), merge.getMappings());
return parse(sender, merge);
}
}
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.search.match.ItemMatchables;
import com.lishid.openinv.search.match.MatchableItem;
import com.lishid.openinv.util.PseudoJson;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemAmountOption implements CompletableOption<MatchableItem> {
@Override
public @NotNull String getName() {
return "amount";
}
@Override
public @Nullable PseudoOption<MatchableItem> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
String minAmountString = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null);
if (minAmountString == null) {
// Amount not correctly specified. Warn about spec.
return null;
}
int minAmount;
try {
minAmount = Integer.parseInt(minAmountString);
} catch (NumberFormatException e) {
// Amount can't be parsed. Warn about spec.
return null;
}
return new PseudoOption<>(pseudoJson, ItemMatchables.hasAmount(minAmount));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
// TODO combine with type
if (!pseudoJson.getIdentifier().equals(getName())) {
// Suggest name while unformed.
if (StringUtil.startsWithIgnoreCase(getName(), pseudoJson.getIdentifier())) {
return Set.of(getName() + ":");
}
// If name is not a valid suggestion, suggest nothing.
return Set.of();
}
String minAmountString = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse("");
// Find and suggest name in full pseudojson.
return TabCompleter.completeInteger(minAmountString).stream().map(match -> {
PseudoJson amount = new PseudoJson(getName());
amount.put(getName(), match);
return amount;
}).map(PseudoJson::asString).collect(Collectors.toSet());
}
@Override
public boolean isUnique() {
return true;
}
}
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.github.jikoo.planarwrappers.util.StringConverters;
import com.lishid.openinv.search.match.ItemMatchables;
import com.lishid.openinv.search.match.MatchableMeta;
import com.lishid.openinv.util.PseudoJson;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemEnchantOption implements CompletableOption<MatchableMeta> {
@Override
public @NotNull String getName() {
return "enchant";
}
@Override
public @Nullable PseudoOption<MatchableMeta> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
Enchantment enchantment = null;
Integer enchantLevel = null;
for (Map.Entry<String, String> entry : pseudoJson.getMappings().entrySet()) {
if (entry.getValue().isBlank()) {
continue;
}
if (entry.getKey().indexOf('l') >= 0) {
try {
enchantLevel = Integer.parseInt(entry.getValue());
} catch (NumberFormatException e) {
// Level can't be parsed. Warn about spec.
return null;
}
}
if (entry.getKey().indexOf('t') >= 0) {
enchantment = StringConverters.toEnchant(entry.getValue());
if (enchantment == null) {
// Enchantment is specified but can't be parsed. Warn about spec.
return null;
}
}
}
return new PseudoOption<>(pseudoJson, ItemMatchables.hasEnchant(enchantment, enchantLevel));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
if (!pseudoJson.getIdentifier().equals(getName())) {
if (!StringUtil.startsWithIgnoreCase(getName(), pseudoJson.getIdentifier())) {
// If enchant is not a valid suggestion, suggest nothing.
return Set.of();
}
// Suggest all options while unformed.
Set<String> set = new HashSet<>();
PseudoJson enchant = new PseudoJson(getName());
set.add(enchant.asString());
enchant.put("type", "");
set.add(enchant.asString());
enchant.put("level", "");
set.add(enchant.asString());
enchant.remove("type");
set.add(enchant.asString());
return set;
}
Collection<String> enchantments = null;
Integer enchantLevel = null;
for (Map.Entry<String, String> entry : pseudoJson.getMappings().entrySet()) {
if (entry.getValue().isBlank()) {
continue;
}
if (entry.getKey().indexOf('l') >= 0) {
try {
enchantLevel = Integer.parseInt(entry.getValue());
} catch (NumberFormatException e) {
// Level is present but cannot be parsed. Default 0.
enchantLevel = 0;
}
}
if (entry.getKey().indexOf('t') >= 0) {
Enchantment enchant = StringConverters.toEnchant(entry.getValue());
if (enchant == null) {
// Enchantment is specified but can't be parsed, tab complete.
enchantments = TabCompleter.completeKeyed(entry.getValue(), Enchantment.values());
} else {
enchantments = Set.of(entry.getValue());
}
}
}
PseudoJson enchant = new PseudoJson(getName());
if (enchantLevel != null) {
enchant.put("level", String.valueOf(enchantLevel));
}
if (enchantments == null) {
return Set.of(enchant.asString());
}
return enchantments.stream().map(enchantment -> {
// Reuse is fine, single-threaded.
enchant.put("type", enchantment);
return enchant;
}).map(PseudoJson::asString).collect(Collectors.toSet());
}
@Override
public boolean isUnique() {
return false;
}
}
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.github.jikoo.planarwrappers.util.StringConverters;
import com.lishid.openinv.search.match.ItemMatchables;
import com.lishid.openinv.search.match.MatchableItem;
import com.lishid.openinv.util.PseudoJson;
import com.lishid.openinv.util.TabCompleter;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.util.StringUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class ItemTypeOption implements CompletableOption<MatchableItem> {
@Override
public @NotNull String getName() {
return "type";
}
@Override
public @Nullable PseudoOption<MatchableItem> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
String typeName = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null);
Material material = StringConverters.toMaterial(typeName);
if (material == null) {
// Invalid material, warn about matcher spec.
return null;
}
return new PseudoOption<>(pseudoJson, ItemMatchables.isType(material));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
if (!pseudoJson.getIdentifier().equals(getName())) {
// Suggest name while unformed.
if (StringUtil.startsWithIgnoreCase(getName(), pseudoJson.getIdentifier())) {
return Set.of(getName() + ":");
}
// If name is not a valid suggestion, suggest nothing.
return Set.of();
}
String typeName = pseudoJson.getMappings().entrySet().stream().findFirst().map(Map.Entry::getValue).orElse("");
// Find and suggest matching types in full pseudojson.
return TabCompleter.completeMaterial(typeName).stream().map(match -> {
PseudoJson type = new PseudoJson(getName());
type.put(getName(), match);
return type;
}).map(PseudoJson::asString).collect(Collectors.toSet());
}
@Override
public boolean isUnique() {
return true;
}
}
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.commands.search;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.bucket.PlayerBucket;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.util.Permissions;
import com.lishid.openinv.util.PseudoJson;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class PlayerSearchOption implements CompletableOption<SearchBucket> {
private final OpenInv plugin;
public PlayerSearchOption(OpenInv plugin) {
this.plugin = plugin;
}
@Override
public @NotNull String getName() {
return "player";
}
@Override
public boolean matches(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
return CompletableOption.super.matches(sender, pseudoJson)
&& Permissions.SEARCH_PLAYERS_ONLINE.hasPermission(sender);
}
@Override
public @Nullable PseudoOption<SearchBucket> parse(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
Optional<Boolean> optional = pseudoJson.getMappings().entrySet().stream()
.filter(entry -> entry.getKey().contains("offline"))
.findFirst()
.map(Map.Entry::getValue)
.map(Boolean::parseBoolean);
return new PseudoOption<>(
pseudoJson, new PlayerBucket(plugin,
!(optional.orElse(false) && Permissions.SEARCH_PLAYERS_OFFLINE.hasPermission(sender))));
}
@Override
public @NotNull Collection<String> suggestOptions(@NotNull CommandSender sender, @NotNull PseudoJson pseudoJson) {
// TODO
return Set.of();
}
@Override
public boolean isUnique() {
return true;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,16 +14,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.util.lang;
package com.lishid.openinv.commands.search;
import com.lishid.openinv.util.PseudoJson;
import org.jetbrains.annotations.NotNull;
/**
* A data holder for string replacement in translations.
* Data holder for completable options.
*
* @param placeholder the placeholder to be replaced
* @param value the value to insert
* @param <T> the type of data
*/
public record Replacement(@NotNull String placeholder, @NotNull String value) {
}
public record PseudoOption<T>(@NotNull PseudoJson pseudoJson, @NotNull T option) {}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -22,7 +22,7 @@ import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public interface PlayerManager {
public interface IPlayerDataManager {
/**
* Loads a Player for an OfflinePlayer.
@@ -51,4 +51,16 @@ public interface PlayerManager {
*/
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
/**
* Convert a raw slot number into a player inventory slot number.
*
* <p>Note that this method is specifically for converting an ISpecialPlayerInventory slot number into a regular
* player inventory slot number.
*
* @param view the open inventory view
* @param rawSlot the raw slot in the view
* @return the converted slot number
*/
int convertToPlayerSlot(InventoryView view, int rawSlot);
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,6 +16,7 @@
package com.lishid.openinv.internal;
import com.lishid.openinv.OpenInv;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
@@ -25,18 +26,17 @@ import org.jetbrains.annotations.NotNull;
public class OpenInventoryView extends InventoryView {
private final @NotNull Player player;
private final @NotNull ISpecialInventory inventory;
private final @NotNull String originalTitle;
private final Player player;
private final ISpecialInventory inventory;
private final String titleKey;
private final String titleDefaultSuffix;
private String title;
public OpenInventoryView(
@NotNull Player player,
@NotNull ISpecialInventory inventory,
@NotNull String originalTitle) {
public OpenInventoryView(Player player, ISpecialInventory inventory, String titleKey, String titleDefaultSuffix) {
this.player = player;
this.inventory = inventory;
this.originalTitle = originalTitle;
this.titleKey = titleKey;
this.titleDefaultSuffix = titleDefaultSuffix;
}
@Override
@@ -61,17 +61,23 @@ public class OpenInventoryView extends InventoryView {
@Override
public @NotNull String getTitle() {
return title == null ? originalTitle : title;
}
if (title == null) {
HumanEntity owner = inventory.getPlayer();
@Override
public @NotNull String getOriginalTitle() {
return originalTitle;
}
String localTitle = OpenInv.getPlugin(OpenInv.class)
.getLocalizedMessage(
owner,
titleKey,
"%player%",
owner.getName());
if (localTitle != null) {
title = localTitle;
} else {
title = owner.getName() + titleDefaultSuffix;
}
}
@Override
public void setTitle(@NotNull String title) {
this.title = title;
return title;
}
}
@@ -0,0 +1,205 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.listeners;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.util.InventoryAccess;
import com.lishid.openinv.util.Permissions;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.bukkit.GameMode;
import org.bukkit.entity.HumanEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryAction;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.inventory.InventoryInteractEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Listener for inventory-related events to prevent modification of inventories where not allowed.
*
* @author Jikoo
*/
public class InventoryListener implements Listener {
private final OpenInv plugin;
public InventoryListener(final OpenInv plugin) {
this.plugin = plugin;
}
@EventHandler
public void onInventoryClose(@NotNull final InventoryCloseEvent event) {
if (!(event.getPlayer() instanceof Player)) {
return;
}
Player player = (Player) event.getPlayer();
if (this.plugin.getPlayerSilentChestStatus(player)) {
this.plugin.getAnySilentContainer().deactivateContainer(player);
}
}
@EventHandler(priority = EventPriority.LOWEST)
public void onInventoryClick(@NotNull final InventoryClickEvent event) {
if (handleInventoryInteract(event)) {
return;
}
// Safe cast - has to be a player to be the holder of a special player inventory.
Player player = (Player) event.getWhoClicked();
if (event.getAction() != InventoryAction.MOVE_TO_OTHER_INVENTORY) {
// All own-inventory interactions require updates to display properly.
// Update in same tick after event completion.
this.plugin.getServer().getScheduler().runTask(this.plugin, player::updateInventory);
return;
}
// Extra handling for MOVE_TO_OTHER_INVENTORY - apparently Mojang no longer removes the item from the target
// inventory prior to adding it to existing stacks.
ItemStack currentItem = event.getCurrentItem();
if (currentItem == null) {
// Other plugin doing some sort of handling (would be NOTHING for null item otherwise), ignore.
return;
}
ItemStack clone = currentItem.clone();
event.setCurrentItem(null);
// Complete add action in same tick after event completion.
this.plugin.getServer().getScheduler().runTask(this.plugin, () -> {
player.getInventory().addItem(clone);
player.updateInventory();
});
}
@EventHandler(priority = EventPriority.LOWEST)
public void onInventoryDrag(@NotNull final InventoryDragEvent event) {
if (handleInventoryInteract(event)) {
return;
}
InventoryView view = event.getView();
int topSize = view.getTopInventory().getSize();
// Get bottom inventory active slots as player inventory slots.
Set<Integer> slots = event.getRawSlots().stream()
.filter(slot -> slot >= topSize)
.map(slot -> plugin.convertToPlayerSlot(view, slot)).collect(Collectors.toSet());
int overlapLosses = 0;
// Count overlapping slots.
for (Map.Entry<Integer, ItemStack> newItem : event.getNewItems().entrySet()) {
int rawSlot = newItem.getKey();
// Skip bottom inventory slots.
if (rawSlot >= topSize) {
continue;
}
int convertedSlot = plugin.convertToPlayerSlot(view, rawSlot);
if (slots.contains(convertedSlot)) {
overlapLosses += getCountDiff(view.getItem(rawSlot), newItem.getValue());
}
}
// Allow no overlap to proceed as usual.
if (overlapLosses < 1) {
return;
}
ItemStack cursor = event.getCursor();
if (cursor != null) {
cursor.setAmount(cursor.getAmount() + overlapLosses);
} else {
cursor = event.getOldCursor().clone();
cursor.setAmount(overlapLosses);
}
event.setCursor(cursor);
}
private int getCountDiff(@Nullable ItemStack original, @NotNull ItemStack result) {
if (original == null || original.getType() != result.getType()) {
return result.getAmount();
}
return result.getAmount() - original.getAmount();
}
/**
* Handle common InventoryInteractEvent functions.
*
* @param event the InventoryInteractEvent
* @return true unless the top inventory is the holder's own inventory
*/
private boolean handleInventoryInteract(@NotNull final InventoryInteractEvent event) {
HumanEntity entity = event.getWhoClicked();
// Un-cancel spectator interactions.
if (Permissions.SPECTATE.hasPermission(entity) && entity.getGameMode() == GameMode.SPECTATOR) {
event.setCancelled(false);
}
if (event.isCancelled()) {
return true;
}
Inventory inventory = event.getView().getTopInventory();
// Is the inventory a special ender chest?
if (InventoryAccess.isEnderChest(inventory)) {
// Disallow ender chest interaction for users without edit permission.
if (!Permissions.EDITENDER.hasPermission(entity)) {
event.setCancelled(true);
}
return true;
}
ISpecialPlayerInventory playerInventory = InventoryAccess.getPlayerInventory(inventory);
// Ignore inventories other than special player inventories.
if (playerInventory == null) {
return true;
}
// Disallow player inventory interaction for users without edit permission.
if (!Permissions.EDITINV.hasPermission(entity)) {
event.setCancelled(true);
return true;
}
// Only specially handle actions in the player's own inventory.
return !event.getWhoClicked().equals(playerInventory.getPlayer());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2011-2022 lishid. All rights reserved.
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -14,11 +14,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv;
package com.lishid.openinv.listeners;
import com.google.errorprone.annotations.Keep;
import com.lishid.openinv.OpenInv;
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;
@@ -29,39 +28,32 @@ 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 {
public class PlayerListener implements Listener {
private final @NotNull OpenInv plugin;
private final @NotNull LanguageManager lang;
private final OpenInv plugin;
PlayerListener(@NotNull OpenInv plugin, @NotNull LanguageManager lang) {
this.plugin = plugin;
this.lang = lang;
}
public PlayerListener(OpenInv plugin) {
this.plugin = plugin;
}
@Keep
@EventHandler(priority = EventPriority.LOWEST)
private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
public void onPlayerJoin(final PlayerJoinEvent event) {
plugin.setPlayerOnline(event.getPlayer());
}
@Keep
@EventHandler(priority = EventPriority.MONITOR)
private void onPlayerQuit(@NotNull PlayerQuitEvent event) {
public void onPlayerQuit(PlayerQuitEvent event) {
plugin.setPlayerOffline(event.getPlayer());
}
@Keep
@EventHandler
private void onWorldChange(@NotNull PlayerChangedWorldEvent event) {
public void onWorldChange(PlayerChangedWorldEvent event) {
plugin.changeWorld(event.getPlayer());
}
@Keep
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
private void onPlayerInteract(@NotNull PlayerInteractEvent event) {
public void onPlayerInteract(PlayerInteractEvent event) {
// Do not cancel 3rd party plugins' custom events
if (!PlayerInteractEvent.class.equals(event.getClass())) {
@@ -75,24 +67,24 @@ class PlayerListener implements Listener {
}
Player player = event.getPlayer();
boolean any = Permissions.CONTAINER_ANY.hasPermission(player) && plugin.getAnyContainerStatus(player);
boolean needsAny = plugin.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock());
boolean any = Permissions.ANYCHEST.hasPermission(player) && plugin.getPlayerAnyChestStatus(player);
boolean needsAny = plugin.getAnySilentContainer().isAnyContainerNeeded(player, event.getClickedBlock());
if (!any && needsAny) {
return;
}
boolean silent = Permissions.CONTAINER_SILENT.hasPermission(player) && plugin.getSilentContainerStatus(player);
boolean silent = Permissions.SILENT.hasPermission(player) && plugin.getPlayerSilentChestStatus(player);
// If anycontainer or silentcontainer is active
if (any || silent) {
if (plugin.getAnySilentContainer().activateContainer(player, silent, event.getClickedBlock())) {
if (silent && 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");
if (silent && plugin.notifySilentChest() && needsAny && plugin.notifyAnyChest()) {
plugin.sendSystemMessage(player, "messages.info.containerBlockedSilent");
} else if (needsAny && plugin.notifyAnyChest()) {
plugin.sendSystemMessage(player, "messages.info.containerBlocked");
} else if (silent && plugin.notifySilentChest()) {
plugin.sendSystemMessage(player, "messages.info.containerSilent");
}
}
event.setCancelled(true);
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.listeners;
import com.lishid.openinv.OpenInv;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.server.PluginDisableEvent;
/**
* Listener for plugin-related events.
*
* @author Jikoo
*/
public class PluginListener implements Listener {
private final OpenInv plugin;
public PluginListener(OpenInv plugin) {
this.plugin = plugin;
}
@EventHandler
public void onPluginDisable(PluginDisableEvent event) {
plugin.releaseAllPlayers(event.getPlugin());
}
}
@@ -0,0 +1,210 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.bucket.SearchBucket;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import com.lishid.openinv.util.MessagePart;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* A search function.
*/
public class Search {
public static final BaseComponent SEPARATOR = new TextComponent(", ");
private final Set<MessagePart> matches = new HashSet<>();
private final ItemMatcher matcher;
private final double totalMatchables;
private final Iterator<SearchBucket> bucketIterator;
private final Runnable callback;
private SearchBucket current = null;
private int polls = 0;
public Search(
@NotNull ItemMatcher matcher,
@NotNull Collection<@NotNull SearchBucket> buckets,
@Nullable Runnable callback) {
// Just in case, copy contents to new set.
Set<SearchBucket> bucketSet = new HashSet<>(buckets);
this.matcher = matcher;
this.totalMatchables = bucketSet.stream().mapToInt(SearchBucket::size).sum();
this.bucketIterator = bucketSet.iterator();
this.callback = callback;
}
private boolean isComplete(OpenInv plugin) {
if (matches.size() >= plugin.getSearchResultsMax()) {
return true;
}
if (current != null && current.hasNext()) {
return false;
}
while (bucketIterator.hasNext()) {
current = bucketIterator.next();
if (current.hasNext()) {
return false;
}
}
return true;
}
/**
* Search the configured amount of Matchables.
*
* <p>Matchables are assembled asynchronously, then
*
* @param plugin the OpenInv instance
*/
private void poll(OpenInv plugin) {
Collection<Matchable> syncMatchables = new ArrayList<>();
// Assemble matchables async, then search synchronously.
for (int i = 0; !isComplete(plugin) && i < plugin.getSearchPollsPerTick(); ++i) {
++polls;
// Calling #isComplete will advance to next InventoryProvider as necessary.
// Bucket matchables for searching synchronously.
syncMatchables.add(current.next());
}
Future<Boolean> future = plugin.getServer().getScheduler().callSyncMethod(plugin, () -> {
for (Matchable matchable : syncMatchables) {
MatchResult match = matchable.match(matcher);
if (match.isMatch()) {
matches.addAll(Arrays.asList(match.getMatch()));
}
}
return true;
});
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
// Eat exception to prevent spam.
}
}
private void sendProgress(CommandSender sender, OpenInv plugin) {
if (!(sender instanceof Player) || polls % (10 * plugin.getSearchPollsPerTick()) != 1) {
return;
}
double progress = 100 * Math.min(polls, totalMatchables) / totalMatchables;
DecimalFormat percent = new DecimalFormat("00.0");
plugin.sendSystemMessage((Player) sender, "messages.info.search.progress",
"%progress%", percent.format(progress),
"%matches%", String.valueOf(matches.size()));
}
private void sendResults(CommandSender sender, OpenInv plugin) {
if (matches.isEmpty()) {
String localizedMessage = plugin.getLocalizedMessage(sender, "messages.info.search.noMatches");
if (localizedMessage == null) {
localizedMessage = "No items found with search parameters.";
}
sender.sendMessage(localizedMessage);
return;
}
String prefix = plugin.getLocalizedMessage(sender, "messages.info.search.matches");
if (prefix == null) {
prefix = "Locations of items matching parameters:";
}
if (sender instanceof Player) {
TextComponent textComponent = new TextComponent(prefix);
Iterator<MessagePart> iterator = matches.iterator();
int total = 0;
int searchResultsMax = plugin.getSearchResultsMax();
while (iterator.hasNext()) {
textComponent.addExtra(iterator.next().forPlayer());
++total;
if (total >= searchResultsMax) {
break;
}
if (iterator.hasNext()) {
textComponent.addExtra(SEPARATOR);
}
}
// Paper deprecated Spigot's message API.
// This is a risky deprecation ignore - Spigot may eventually do the sane thing
// and just add a method accepting components to the player directly.
//noinspection deprecation
sender.spigot().sendMessage(textComponent);
return;
}
String message = matches.stream()
.limit(plugin.getSearchResultsMax())
.map(MessagePart::forConsole)
.collect(Collectors.joining(", ", prefix, ""));
sender.sendMessage(message);
}
public void schedule(CommandSender sender, OpenInv plugin) {
new BukkitRunnable() {
@Override
public void run() {
// Scheduler struggles to cancel async tasks with high frequency.
if (isCancelled()) {
return;
}
poll(plugin);
if (isComplete(plugin)) {
if (isCancelled()) {
return;
}
sendResults(sender, plugin);
cancel();
if (callback != null) {
callback.run();
}
return;
}
sendProgress(sender, plugin);
}
}.runTaskTimerAsynchronously(plugin, 0L, 1L);
}
}
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.jetbrains.annotations.NotNull;
public class ChunkBucket implements SearchBucket {
private final World world;
private final boolean load;
private final List<ChunkCoord> chunks;
protected int index = -1;
private Boolean paper = null;
public ChunkBucket(@NotNull World world, int chunkX, int chunkZ, int radius, boolean load) {
this.world = world;
this.load = load;
this.chunks = new ArrayList<>();
// Order chunks based (loosely) on proximity.
spiralRectangle(radius, (deltaX, deltaZ) -> chunks.add(new ChunkCoord(chunkX + deltaX, chunkZ + deltaZ)));
}
@Override
public @NotNull Matchable next() throws IndexOutOfBoundsException {
ChunkCoord chunkCoord = chunks.get(++index);
if (!world.isChunkGenerated(chunkCoord.x(), chunkCoord.z())) {
return Matchable.EMPTY;
}
if (!load && !world.isChunkLoaded(chunkCoord.x(), chunkCoord.z())) {
return Matchable.EMPTY;
}
// If async chunk loading is not available, load chunk when checking.
if (!isPaper()) {
return new MatchableChunk(world, chunkCoord.x(), chunkCoord.z());
}
CompletableFuture<Chunk> chunkAt = world.getChunkAtAsync(chunkCoord.x(), chunkCoord.z());
Chunk chunk;
try {
chunk = chunkAt.get();
} catch (InterruptedException | ExecutionException e) {
return matcher -> MatchResult.NO_MATCH;
}
return new MatchableChunk(chunk);
}
private boolean isPaper() {
if (paper == null) {
try {
// Check for Paper.
Class.forName("com.destroystokyo.paper.PaperConfig");
paper = true;
} catch (ClassNotFoundException e) {
paper = false;
}
}
return paper;
}
@Override
public boolean hasNext() {
return index < chunks.size() - 1;
}
@Override
public int size() {
return chunks.size();
}
/**
* Produce a series of integer coordinates starting at 0, 0 that extend outward to form a centered square,
* producing closer proximity results sooner than a traditional double for loop from min to max.
*
* @param radius the radius of the square
* @param coordConsumer the method consuming the values produced
*/
private static void spiralRectangle(int radius, BiConsumer<Integer, Integer> coordConsumer) {
for (int x = 0; x <= radius; ++x) {
for (int z = 0; z <= radius; ++z) {
coordConsumer.accept(x, z);
if (x != 0) {
coordConsumer.accept(-x, z);
if (z != 0) {
coordConsumer.accept(-x, -z);
}
}
if (z != 0) {
coordConsumer.accept(x, -z);
}
}
}
}
}
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
/**
* A data holder for chunk coordinates.
*/
public record ChunkCoord(int x, int z) {}
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import com.lishid.openinv.util.MessagePart;
import java.util.ArrayList;
import java.util.List;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.Chunk;
import org.bukkit.World;
import org.bukkit.block.BlockState;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
class MatchableChunk implements Matchable {
private Chunk chunk;
private World world;
private int chunkX;
private int chunkZ;
MatchableChunk(@NotNull Chunk chunk) {
this.chunk = chunk;
}
MatchableChunk(@NotNull World world, int chunkX, int chunkZ) {
this.world = world;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
private Chunk getChunk() {
if (chunk == null) {
chunk = world.getChunkAt(chunkX, chunkZ);
}
return chunk;
}
@Override
public @NotNull MatchResult match(@NotNull ItemMatcher matcher) {
List<BlockState> blocks = new ArrayList<>();
for (BlockState tileEntity : getChunk().getTileEntities()) {
if (!(tileEntity instanceof InventoryHolder)) {
continue;
}
Inventory inventory = ((InventoryHolder) tileEntity).getInventory();
for (ItemStack content : inventory.getContents()) {
if (matcher.matches(content)) {
blocks.add(tileEntity);
break;
}
}
}
if (blocks.isEmpty()) {
return MatchResult.NO_MATCH;
}
MessagePart[] matches = new MessagePart[blocks.size()];
for (int i = 0; i < matches.length; ++i) {
BlockState state = blocks.get(i);
matches[i] = new MessagePart(() -> getComponent(state), () -> getString(state));
}
return new MatchResult(matches);
}
private static BaseComponent getComponent(BlockState state) {
TextComponent component = new TextComponent(state.getType().name());
String command = String.format(
"/execute in %s minecraft:tp %s %s %s",
state.getWorld().getName(),
state.getX(),
state.getY() + 1,
state.getZ());
component.setClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, command));
String hover = String.format(
"%s{world:%s,x:%s,y:%s,z:%s}\n\nClick to teleport.",
state.getType().getKey(),
state.getWorld().getName(),
state.getX(),
state.getY(),
state.getZ());
Text hoverText = new Text(new BaseComponent[] { new TextComponent(hover)});
component.setHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverText));
return component;
}
private static String getString(BlockState state) {
return String.format(
"%s{world:%s,x:%s,y:%s,z:%s}",
state.getType().getKey(),
state.getWorld().getName(),
state.getX(),
state.getY(),
state.getZ());
}
}
@@ -0,0 +1,174 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.match.ItemMatcher;
import com.lishid.openinv.search.match.MatchResult;
import com.lishid.openinv.search.match.Matchable;
import com.lishid.openinv.util.MessagePart;
import java.util.UUID;
import java.util.function.Supplier;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.ClickEvent;
import net.md_5.bungee.api.chat.HoverEvent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.api.chat.hover.content.Text;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
public class MatchablePlayer implements Matchable {
private final OpenInv plugin;
private final OfflinePlayer offlinePlayer;
public MatchablePlayer(OpenInv plugin, OfflinePlayer offlinePlayer) {
this.plugin = plugin;
this.offlinePlayer = offlinePlayer;
}
@Override
public @NotNull MatchResult match(@NotNull ItemMatcher matcher) {
Player player;
if (offlinePlayer instanceof Player) {
player = (Player) offlinePlayer;
} else {
player = plugin.loadPlayer(offlinePlayer);
}
if (player == null) {
return MatchResult.NO_MATCH;
}
boolean matchInv = false;
boolean matchEnder = false;
PlayerInventory playerInventory = player.getInventory();
// Check inventory. Modern implementations include armor and off hand in contents.
for (ItemStack content : playerInventory.getContents()) {
if (matcher.matches(content)) {
matchInv = true;
break;
}
}
InventoryView openInventory = player.getOpenInventory();
// If no matches are found yet, check cursor.
if (!matchInv && matcher.matches(openInventory.getCursor())) {
matchInv = true;
}
// Check player crafting inventory.
if (!matchInv && openInventory.getType() == InventoryType.CRAFTING) {
for (ItemStack content : openInventory.getTopInventory().getContents()) {
if (matcher.matches(content)) {
matchInv = true;
break;
}
}
}
// Check ender chest.
for (ItemStack content : player.getEnderChest().getContents()) {
if (matcher.matches(content)) {
matchEnder = true;
break;
}
}
if (!matchInv && !matchEnder) {
return MatchResult.NO_MATCH;
}
String name = player.getName();
UUID uuid = player.getUniqueId();
if (matchInv && matchEnder) {
return new MatchResult(new MessagePart[] {
new MessagePart(() -> getBoth(name, uuid), () -> name + " (inv, ender)")
});
}
Supplier<BaseComponent> componentSupplier;
Supplier<String> stringSupplier;
if (matchInv) {
componentSupplier = () -> getSingle(name, () -> getInvClick(uuid), () -> getInvHover(name));
stringSupplier = () -> name + " (inv)";
} else {
componentSupplier = () -> getSingle(name, () -> getEnderClick(uuid), () -> getEnderHover(name));
stringSupplier = () -> name + " (ender)";
}
return new MatchResult(new MessagePart[] { new MessagePart(componentSupplier, stringSupplier) });
}
private static BaseComponent getBoth(String name, UUID uuid) {
BaseComponent component = new TextComponent(name + " (inv,");
component.setClickEvent(getInvClick(uuid));
component.setHoverEvent(getInvHover(name));
BaseComponent enderComponent = new TextComponent(" ender)");
enderComponent.setClickEvent(getEnderClick(uuid));
enderComponent.setHoverEvent(getEnderHover(name));
component.addExtra(enderComponent);
return component;
}
private static BaseComponent getSingle(String name, Supplier<ClickEvent> click, Supplier<HoverEvent> hover) {
BaseComponent component = new TextComponent(name);
component.setClickEvent(click.get());
component.setHoverEvent(hover.get());
return component;
}
private static ClickEvent getInvClick(UUID uuid) {
return getClick("/openinv:openinv ", uuid);
}
private static HoverEvent getInvHover(String name) {
return getHover("/openinv ", name);
}
private static ClickEvent getEnderClick(UUID uuid) {
return getClick("/openinv:openender ", uuid);
}
private static HoverEvent getEnderHover(String name) {
return getHover("/openender ", name);
}
private static ClickEvent getClick(String command, UUID uuid) {
// Actual command run is not exposed to user, so there's no reason not to use UUID and be precise.
return new ClickEvent(ClickEvent.Action.RUN_COMMAND, command + uuid);
}
private static HoverEvent getHover(String command, String name) {
BaseComponent[] text = new BaseComponent[] { new TextComponent(command + name) };
return new HoverEvent(HoverEvent.Action.SHOW_TEXT, new Text(text));
}
}
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2011-2021 lishid. All rights reserved.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.lishid.openinv.search.bucket;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.search.match.Matchable;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
public class PlayerBucket implements SearchBucket {
private final OpenInv plugin;
private final OfflinePlayer[] players;
private int index = -1;
public PlayerBucket(OpenInv plugin, boolean online) {
this.plugin = plugin;
if (online) {
this.players = Bukkit.getOnlinePlayers().toArray(new Player[0]);
} else {
this.players = Bukkit.getOfflinePlayers();
}
}
@Override
public boolean hasNext() {
return index < players.length - 1;
}
@Override
public @NotNull Matchable next() {
return new MatchablePlayer(plugin, players[++index]);
}
@Override
public int size() {
return 1;
}
}
Loaded 100 of 151 files, more files were not shown because too many files have changed in this diff. Show more