Compare commits

...
42 Commits
Author SHA1 Message Date
Jikoo 63fccdcf91 Bump version to 5.3.0 for release 2026-01-17 23:57:38 -05:00
dependabot[bot]andJikoo 960acbfe9d Bump errorprone versions (#370)
* Bump com.google.errorprone:error_prone_core from 2.45.0 to 2.46.0

Bumps [com.google.errorprone:error_prone_core](https://github.com/google/error-prone) from 2.45.0 to 2.46.0.
- [Release notes](https://github.com/google/error-prone/releases)
- [Commits](https://github.com/google/error-prone/compare/v2.45.0...v2.46.0)

---
updated-dependencies:
- dependency-name: com.google.errorprone:error_prone_core
  dependency-version: 2.46.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Bump errorprone-gradle-plugin to 4.4.0

Resolves issues with missing flag required for errorprone-core

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jikoo <jikoo.games@gmail.com>
2026-01-18 02:37:20 +00:00
dependabot[bot] 3c904499c5 Bump com.gradleup.shadow from 9.2.2 to 9.3.1 (#369) 2026-01-17 16:54:05 +00:00
Adam baa6b3d734 Add profile cache (#368)
Instead of reading every single player file from disk when doing a fuzzy match, we now do a single lookup from a SQLite database. The database is populated once with all existing players' names on startup, then maintained when players join. This is a massive performance boost for servers that have high player counts or run on spinning disks.

Lookups now require that the first character of the target player's name be correct (case-insensitive). For Geyser-like names, this extends to the first character of their real name. For example, "XxXJohnMinecraftXxX" can no longer be located by a "johnminecraft" search. A search for "xjohnminecraft" could be used instead. A Geyser player ".FranMinecraft" would require ".f" to be eligible, i.e. "franmc" cannot produce the correct result while ".franmc" might.
2026-01-17 11:50:23 -05:00
Adam 7f6c339e06 Add option to check permissions with toggle (#361)
* Promote Permissions to API
* Document permissions
* Add method for checking a toggle with permissions
2026-01-03 12:20:58 -05:00
dependabot[bot] 00736c8d30 Bump actions/download-artifact from 6 to 7 (#364)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 6 to 7.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 12:12:35 -05:00
dependabot[bot] cb1ec77c24 Bump actions/upload-artifact from 5 to 6 (#363)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 12:12:25 -05:00
dependabot[bot] c8c8a72819 Bump actions/cache from 4 to 5 (#362)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-01 12:12:14 -05:00
Alessandro 16f91ddc9f feat: add use sub-permissions 2026-01-01 08:55:43 -05:00
Adam c83511e475 Upload to Modrinth and Hangar (#360)
* Replace curseforge-upload with mc-publish
* Use anchors for common configurations
* Convert script to a shared run step
2025-12-31 14:39:53 -05:00
Jikoo c095a78013 Bump version to 5.2.1-SNAPSHOT for development 2025-12-17 15:29:26 -05:00
Jikoo 4441cdcf04 Bump version to 5.2.0 for release 2025-12-17 15:29:23 -05:00
Adam 9940b9d840 Update to 1.21.11 (#358) 2025-12-17 15:11:56 -05:00
dependabot[bot] fb409f8024 Bump actions/checkout from 5 to 6 (#354)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-14 17:48:28 -05:00
dependabot[bot] 08b43a8aca Bump softprops/action-gh-release from 2.4.1 to 2.5.0 (#355)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.4.1 to 2.5.0.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.4.1...v2.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-14 17:08:30 -05:00
dependabot[bot] 3df3d41a7f Bump com.google.errorprone:error_prone_core from 2.43.0 to 2.45.0 (#356) 2025-12-01 20:21:44 +00:00
dependabot[bot] 16cba8a137 Bump actions/upload-artifact from 4 to 5 (#348)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 09:59:45 -05:00
dependabot[bot] f1b86b4eb1 Bump softprops/action-gh-release from 2.3.3 to 2.4.1 (#349)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.3 to 2.4.1.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.3.3...v2.4.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 09:59:24 -05:00
dependabot[bot] 172e1cd55b Bump actions/download-artifact from 5 to 6 (#350)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 09:58:40 -05:00
Adam d8b5aa145b Improve equal access behaviors (#346)
* Extract common chest code, expose view-only state
* Add prefix for view-only state
* Add nodes for behavior of accessing equal
* Include permission-based denial feedback
2025-11-03 09:51:00 -05:00
dependabot[bot] 2ffff99acf Bump com.google.errorprone:error_prone_core from 2.42.0 to 2.43.0 (#351) 2025-11-01 17:24:15 +00:00
Jikoo 743c46f322 Bump version to 5.1.16-SNAPSHOT for development 2025-10-11 09:48:58 -04:00
Jikoo e3285a6d61 Bump version to 5.1.15 for release 2025-10-11 09:48:35 -04:00
Jikoo 806fb848a7 Add line break between server implementations
I noticed this was a problem the first update and have been forgetting and then manually fixing it on Bukkit since. A travesty.
2025-10-11 09:46:34 -04:00
Adam b269a5ed3c Fix /openinv on Spigot (#342) 2025-10-11 13:35:59 +00:00
Jikoo 8c102c4400 Bump version to 5.1.15-SNAPSHOT for development 2025-10-08 15:01:55 -04:00
Jikoo 080d02f93f Bump version to 5.1.14 for release 2025-10-08 15:01:31 -04:00
Adam 54f9d30a89 Update to 1.21.9/1.21.10 (#339) 2025-10-08 15:00:13 -04:00
dependabot[bot] 6063959362 Bump actions/github-script from 7 to 8 (#336)
Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8.
- [Release notes](https://github.com/actions/github-script/releases)
- [Commits](https://github.com/actions/github-script/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 09:03:50 -04:00
dependabot[bot] d80b8a6f4d Bump gradle/actions from 4 to 5 (#334)
Bumps [gradle/actions](https://github.com/gradle/actions) from 4 to 5.
- [Release notes](https://github.com/gradle/actions/releases)
- [Commits](https://github.com/gradle/actions/compare/v4...v5)

---
updated-dependencies:
- dependency-name: gradle/actions
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 09:03:22 -04:00
dependabot[bot] 13621901ad Bump softprops/action-gh-release from 2.3.2 to 2.3.3 (#335)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.3.2 to 2.3.3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2.3.2...v2.3.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 08:49:37 -04:00
dependabot[bot] 20cc6e004e Bump com.gradleup.shadow from 9.1.0 to 9.2.2 (#337) 2025-10-01 17:25:00 +00:00
dependabot[bot] 80585dfc07 Bump com.google.errorprone:error_prone_core from 2.41.0 to 2.42.0 (#338) 2025-10-01 17:24:53 +00:00
dependabot[bot] 9e37b42c63 Bump actions/download-artifact from 4 to 5 (#328)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 10:08:58 -04:00
dependabot[bot] 58be5cffc9 Bump actions/setup-java from 4 to 5 (#330)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 13:14:45 +00:00
dependabot[bot] e8c8e625c0 Bump actions/checkout from 4 to 5 (#327)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 09:10:37 -04:00
dependabot[bot] 0ee3f8c1b2 Bump com.gradleup.shadow from 8.3.8 to 9.1.0 (#331) 2025-09-02 12:15:47 +00:00
dependabot[bot] 99f9099981 Bump org.jetbrains:annotations from 26.0.2 to 26.0.2-1 (#329) 2025-09-02 11:19:21 +00:00
Jikoo 70ae432158 Fix missing override annotations 2025-08-07 09:06:35 -04:00
dependabot[bot] ad05f427fd Bump com.google.errorprone:error_prone_core from 2.39.0 to 2.41.0 (#325) 2025-08-01 17:36:44 +00:00
Adam 252e93f872 Update for new Paper changes (#324)
* Fix potential desync if a plugin triggers a slot update under certain circumstances
* Increase visibility to match changes to superclass
2025-07-28 12:55:08 +00:00
Jikoo 0267090cb6 Bump version to 5.1.14-SNAPSHOT for development 2025-07-18 11:43:17 -04:00
75 changed files with 2510 additions and 1429 deletions

No files matched your search

+9
View File
@@ -8,3 +8,12 @@ updates:
directory: "/"
schedule:
interval: "monthly"
ignore:
# The SQLite JDBC is provided by Spigot and, for now, Paper.
# To ensure it is still present even if Paper stops providing it,
# we need to declare it as a library.
# To prevent needlessly downloading (and loading) a second copy,
# we want to ensure that we are using the provided version.
- dependency-name: "org.xerial:sqlite-jdbc"
# Spigot and Spigot API updates are manual.
- dependency-name: "org.spigotmc:*"
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
# 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
uses: actions/github-script@v8
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
+5 -5
View File
@@ -15,9 +15,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: actions/setup-java@v4
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
@@ -25,7 +25,7 @@ jobs:
# We can't use 'maven' prebuilt cache setup because it requires that the project have a pom file.
# BuildTools installs to Maven local if available, so it's easier to just rely on that.
- name: Cache Spigot dependency
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.m2/repository/org/spigotmc/
@@ -33,7 +33,7 @@ jobs:
restore-keys: |
${{ runner.os }}-buildtools-
- uses: gradle/actions/setup-gradle@v4
- uses: gradle/actions/setup-gradle@v5
- name: Build with Gradle
run: ./gradlew clean build
@@ -41,7 +41,7 @@ jobs:
# Upload artifacts
- name: Upload Distributable Jar
id: upload-final
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist
path: ./dist/*
+3 -3
View File
@@ -13,21 +13,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Download build
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist
path: dist
- name: Create Release
id: create-release
uses: softprops/action-gh-release@v2.3.2
uses: softprops/action-gh-release@v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: OpenInv ${{ env.VERSIONED_NAME }}
body: |-
## Supported server versions
**Paper:** TODO VERSION, 1.21.4, 1.21.3, 1.21.1
**Paper:** TODO VERSION, 1.21.8, 1.21.7, 1.21.6, 1.21.5, 1.21.4, 1.21.3, 1.21.1
**Spigot:** TODO VERSION
TODO HELLO HUMAN, PRESS THE GENERATE CHANGELOG BUTTON PLEASE.
+79 -19
View File
@@ -1,4 +1,4 @@
name: Release to CurseForge
name: Release to external sites
on:
release:
@@ -8,30 +8,90 @@ jobs:
curseforge_release:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- &parse-paper-versions
name: Parse Paper Versions
id: parse_versions
run: |
# Find line declaring Paper versions.
raw=$(grep "\\*\\*Paper:\\*\\*" <<< "${{ github.event.release.body }}")
- name: Fetch Github Release Asset
# Enable extended glob pattern to match 0 or more whitespace characters.
shopt -s extglob
# Trim Paper versions identifier prefix.
raw=${raw/*([[:space:]])'**'Paper:'**'*([[:space:]])/}
# Replace commas and optional spaces with a newline.
raw=${raw//,*([[:space:]])/$'\n'}
# Turn extglob back off.
shopt -u extglob
echo "$raw"
printf "PAPER_VERSIONS<<EOF\n%s\nEOF\n" "$raw" >> "$GITHUB_OUTPUT"
- &fetch-release-asset
name: Fetch Github Release Asset
uses: dsaltares/fetch-gh-release-asset@1.1.2
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ github.event.release.id }}
file: OpenInv.jar
- name: Set CurseForge Variables
run: . scripts/set_curseforge_env.sh "${{ github.event.release.body }}"
- name: Create CurseForge Release
uses: itsmeow/curseforge-upload@v3
uses: Kira-NT/mc-publish@v3.3
with:
token: "${{ secrets.CURSEFORGE_TOKEN }}"
project_id: 31432
game_endpoint: minecraft
file_path: ./OpenInv.jar
display_name: "${{ github.event.release.name }}"
game_versions: "${{ env.CURSEFORGE_MINECRAFT_VERSIONS }}"
release_type: release
changelog_type: markdown
changelog: "${{ github.event.release.body }}"
curseforge-token: ${{ secrets.CURSEFORGE_TOKEN }}
curseforge-id: 31432
files: ./OpenInv.jar
game-versions: "${{ steps.parse_versions.outputs.PAPER_VERSIONS }}"
game-version-filter: releases
modrinth_release:
runs-on: ubuntu-latest
steps:
- *parse-paper-versions
- *fetch-release-asset
- name: Create Modrinth Release
uses: Kira-NT/mc-publish@v3.3
with:
modrinth-id: 1UlvXbzL
modrinth-featured: false
modrinth-token: ${{ secrets.MODRINTH_TOKEN }}
files: ./OpenInv.jar
game-versions: "${{ steps.parse_versions.outputs.PAPER_VERSIONS }}"
game-version-filter: releases
hangar_release:
runs-on: ubuntu-latest
steps:
- name: Parse Hangar Paper Versions
id: parse_versions
run: |
# Find line declaring Paper versions.
raw=$(grep "\\*\\*Paper:\\*\\*" <<< "${{ github.event.release.body }}")
# Trim Paper versions identifier prefix.
raw=${raw/'**'Paper:'**'/}
# Generate JSON for Hangar. GitHub Actions provides jq 1.7 in the 24.04 image.
# Trim, then split on comma and optional spaces.
hangar_versions=$(jq -nc --arg versions "$raw" '{"PAPER": $versions | trim | split(", *";null)}')
echo "$hangar_versions"
echo "HANGAR_PAPER_VERSIONS=$hangar_versions" >> "$GITHUB_OUTPUT"
- *fetch-release-asset
- name: Create Hangar Release
uses: benwoo1110/hangar-upload-action@v1
with:
api_token: ${{ secrets.HANGAR_TOKEN }}
slug: 'OpenInv'
version: ${{ github.event.release.tag_name }}
channel: release
files: |-
[
{
"path": "./OpenInv.jar",
"platforms": ["PAPER"]
}
]
platform_dependencies: "${{ steps.parse_versions.outputs.HANGAR_PAPER_VERSIONS }}"
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
run: |
mkdir -p ./pr
echo ${{ github.event.number }} > ./pr/pr_number
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v6
with:
name: pr_number
path: pr/
+2 -2
View File
@@ -13,11 +13,11 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Build resource pack
id: upload-resource-pack
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: openinv-legibility-pack
path: ./resource-pack/openinv-legibility-pack/
@@ -138,14 +138,21 @@ public interface IOpenInv {
boolean online
) throws InstantiationException;
/**
* @deprecated Use {@link #openInventory(Player, ISpecialInventory, boolean)}
*/
@Deprecated(forRemoval = true, since = "5.2.0")
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
/**
* Open an {@link ISpecialInventory} for a {@link Player}.
*
* @param player the viewer
* @param inventory the inventory to open
* @param viewOnly whether the inventory should be view-only
* @return the resulting {@link InventoryView}
*/
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory);
@Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly);
/**
* Check if a {@link Player} is currently loaded by OpenInv.
@@ -0,0 +1,128 @@
/*
* 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;
/**
* An enum containing all permissions directly checked by OpenInv.
*
* <p>Note that this is not an exhaustive list! This does not contain
* all permissions managed by Bukkit, largely parent nodes.</p>
*/
public enum Permissions {
/// Permission to open one's own inventory.
INVENTORY_OPEN_SELF("inventory.open.self"),
/// Permission to open someone else's inventory.
INVENTORY_OPEN_OTHER("inventory.open.other"),
/**
* Permission to edit one's own inventory.
*
* <p>Note that this does not guarantee that the user has access to open the inventory!
* Be sure to check {@link #INVENTORY_OPEN_SELF} first.</p>
*/
INVENTORY_EDIT_SELF("inventory.edit.self"),
/**
* Permission to edit someone else's inventory.
*
* <p>Note that this does not guarantee that the user has access to open the inventory!
* Be sure to check {@link #INVENTORY_OPEN_OTHER} first.</p>
*/
INVENTORY_EDIT_OTHER("inventory.edit.other"),
/// Permission to insert any item into the head slot.
INVENTORY_SLOT_HEAD_ANY("inventory.slot.head.any"),
/// Permission to insert any item into the chest slot.
INVENTORY_SLOT_CHEST_ANY("inventory.slot.chest.any"),
/// Permission to insert any item into the legs slot.
INVENTORY_SLOT_LEGS_ANY("inventory.slot.legs.any"),
/// Permission to insert any item into the feet slot.
INVENTORY_SLOT_FEET_ANY("inventory.slot.feet.any"),
/// Permission to drop items as the player via the drop slot.
INVENTORY_SLOT_DROP("inventory.slot.drop"),
/// Permission to open one's own ender chest.
ENDERCHEST_OPEN_SELF("enderchest.open.self"),
/// Permission to open someone else's ender chest.
ENDERCHEST_OPEN_OTHER("enderchest.open.other"),
/**
* Permission to edit one's own ender chest.
*
* <p>Note that this does not guarantee that the user has access to open the inventory!
* Be sure to check {@link #ENDERCHEST_OPEN_SELF} first.</p>
*/
ENDERCHEST_EDIT_SELF("enderchest.edit.self"),
/**
* Permission to edit someone else's ender chest.
*
* <p>Note that this does not guarantee that the user has access to open the inventory!
* Be sure to check {@link #ENDERCHEST_OPEN_OTHER} first.</p>
*/
ENDERCHEST_EDIT_OTHER("enderchest.edit.other"),
/// Permission to clear one's own inventory.
CLEAR_SELF("clear.self"),
/// Permission to clear someone else's inventory.
CLEAR_OTHER("clear.other"),
/// Permission to view inventories and ender chests from other worlds.
ACCESS_CROSSWORLD("access.crossworld"),
/// Permission to access offline players' inventories and ender chests.
ACCESS_OFFLINE("access.offline"),
/// Permission to access online players' inventories and ender chests.
ACCESS_ONLINE("access.online"),
/// Permission granting the ability to edit players with the same access level.
ACCESS_EQUAL_EDIT("access.equal.edit"),
/// Permission to view, but not edit, players with the same access level.
ACCESS_EQUAL_VIEW("access.equal.view"),
/// Permission to deny access to players with the same access level.
ACCESS_EQUAL_DENY("access.equal.deny"),
/// Permission to perform inventory interaction while in spectator mode.
SPECTATE_CLICK("spectate.click"),
/// Permission to use the AnyContainer feature.
CONTAINER_ANY("container.any"),
/// Permission to use the AnyContainer feature but not the command to toggle it.
CONTAINER_ANY_USE("container.any.use"),
/// Permission to use the SilentContainer feature.
CONTAINER_SILENT("container.silent"),
/// Permission to use the SilentContainer feature but not the command to toggle it.
CONTAINER_SILENT_USE("container.silent.use"),
/// Permission to search inventories and ender chests.
SEARCH_INVENTORY("search.inventory"),
/// Permission to search containers.
SEARCH_CONTAINER("search.container");
private final String permission;
Permissions(String permission) {
this.permission = "openinv." + permission;
}
/**
* Check if a {@link Permissible} has this permission.
*
* @param permissible the Permissible
* @return true if the permission is granted
*/
public boolean hasPermission(@NotNull Permissible permissible) {
return permissible.hasPermission(permission);
}
}
@@ -1,5 +1,6 @@
package com.lishid.openinv.util.setting;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
@@ -24,6 +25,15 @@ public interface PlayerToggle {
*/
boolean is(@NotNull UUID uuid);
/**
* Get the state of the toggle for a particular {@link Player},
* accounting for permissions required to use the feature.
*
* @param player the player
* @return true if the setting is enabled and the player has the required permissions
*/
boolean is(@NotNull Player player);
/**
* Set the state of the toggle for a particular player ID.
*
@@ -1,5 +1,7 @@
package com.lishid.openinv.util.setting;
import com.lishid.openinv.util.Permissions;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnmodifiableView;
@@ -19,8 +21,20 @@ import java.util.UUID;
public final class PlayerToggles {
private static final Map<String, PlayerToggle> TOGGLES = new HashMap<>();
private static final PlayerToggle ANY = add(new MemoryToggle("AnyContainer"));
private static final PlayerToggle SILENT = add(new MemoryToggle("SilentContainer"));
private static final PlayerToggle ANY = add(new MemoryToggle("AnyContainer") {
@Override
public boolean is(@NotNull Player player) {
return is(player.getUniqueId())
&& (Permissions.CONTAINER_ANY.hasPermission(player) || Permissions.CONTAINER_ANY_USE.hasPermission(player));
}
});
private static final PlayerToggle SILENT = add(new MemoryToggle("SilentContainer") {
@Override
public boolean is(@NotNull Player player) {
return is(player.getUniqueId())
&& (Permissions.CONTAINER_SILENT.hasPermission(player) || Permissions.CONTAINER_SILENT_USE.hasPermission(player));
}
});
/**
* Get the AnyContainer toggle.
@@ -72,7 +86,7 @@ public final class PlayerToggles {
throw new IllegalStateException("Cannot create instance of utility class.");
}
private static class MemoryToggle implements PlayerToggle {
private static abstract class MemoryToggle implements PlayerToggle {
private final @NotNull Set<UUID> enabled;
private final @NotNull String name;
@@ -1,63 +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"),
CLEAR_SELF("clear.self"),
CLEAR_OTHER("clear.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 -1
View File
@@ -1,6 +1,6 @@
# Project meta
group=com.lishid.openinv
version=5.1.13
version=5.3.0
description=A Bukkit plugin for opening normally-inaccessible inventories.
# Gradle configuration
+6 -4
View File
@@ -2,13 +2,14 @@
spigotapi = "1.21.5-R0.1-SNAPSHOT"
specialsource = "1.11.5"
planarwrappers = "3.3.0"
annotations = "26.0.2"
annotations = "26.0.2-1"
paperweight = "2.0.0-beta.17"
shadow = "8.3.8"
shadow = "9.3.1"
folia-scheduler-wrapper = "v0.0.3"
errorprone-core = "2.39.0"
errorprone-gradle = "4.2.0"
errorprone-core = "2.46.0"
errorprone-gradle = "4.4.0"
slf4j = "2.0.17"
sqlite-jdbc = "3.49.1.0"
[libraries]
spigotapi = { module = "org.spigotmc:spigot-api", version.ref = "spigotapi" }
@@ -19,6 +20,7 @@ folia-scheduler-wrapper = { module = "com.github.NahuLD.folia-scheduler-wrapper:
errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone-core" }
errorprone-gradle = { module = "net.ltgt.gradle:gradle-errorprone-plugin", version.ref = "errorprone-gradle" }
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" }
[plugins]
paperweight = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" }
+1 -1
View File
@@ -33,7 +33,7 @@ dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT")
paperweight.paperDevBundle("1.21.11-R0.1-SNAPSHOT")
}
val spigot = tasks.register<ShadowJar>("spigotRelocations") {
@@ -0,0 +1,335 @@
package com.lishid.openinv.internal.common.container;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.bukkit.OpenPlayerInventory;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import com.lishid.openinv.internal.common.container.menu.OpenInventoryMenu;
import com.lishid.openinv.internal.common.container.slot.Content;
import com.lishid.openinv.internal.common.container.slot.ContentCrafting;
import com.lishid.openinv.internal.common.container.slot.ContentCraftingResult;
import com.lishid.openinv.internal.common.container.slot.ContentCursor;
import com.lishid.openinv.internal.common.container.slot.ContentDrop;
import com.lishid.openinv.internal.common.container.slot.ContentEquipment;
import com.lishid.openinv.internal.common.container.slot.ContentList;
import com.lishid.openinv.internal.common.container.slot.ContentOffHand;
import com.lishid.openinv.internal.common.container.slot.ContentViewOnly;
import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
import com.lishid.openinv.internal.common.player.PlayerManager;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
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.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.Location;
import org.bukkit.craftbukkit.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.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.Comparator;
import java.util.List;
import java.util.Map;
public abstract class BaseOpenInventory implements Container, InternalOwned<ServerPlayer>, ISpecialPlayerInventory {
protected final List<Content> slots;
private final int size;
protected ServerPlayer owner;
private int maxStackSize = 99;
protected CraftInventory bukkitEntity;
public List<HumanEntity> transaction = new ArrayList<>();
public BaseOpenInventory(@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();
}
protected void setupSlots() {
// Top of inventory: Regular contents.
int nextIndex = addMainInventory();
// If inventory is expected size, we can arrange slots to be pretty.
Inventory ownerInv = owner.getInventory();
if (ownerInv.getNonEquipmentItems().size() == 36
&& owner.inventoryMenu.getCraftSlots().getContainerSize() == 4
&& (Inventory.EQUIPMENT_SLOT_MAPPING.size() == 5 || Inventory.EQUIPMENT_SLOT_MAPPING.size() == 7)) {
// 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().getNonEquipmentItems().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().getNonEquipmentItems();
}
}
);
}
return listSize;
}
private int addArmor(int startIndex) {
// Armor slots go bottom to top; boots are first and helmet is last.
// Since we have to display horizontally due to space restrictions,
// making the left side the "top" is more user-friendly.
EquipmentSlot[] sorted = Inventory.EQUIPMENT_SLOT_MAPPING.int2ObjectEntrySet()
.stream()
.sorted(Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey))
.map(Map.Entry::getValue)
.toArray(EquipmentSlot[]::new);
int localIndex = 0;
for (int i = sorted.length - 1; i >= 0; --i) {
// Skip off-hand, handled separately. Also skip non-player slots.
if (sorted[i].getType() != EquipmentSlot.Type.HUMANOID_ARMOR) {
continue;
}
slots.set(startIndex + localIndex, new ContentEquipment(owner, sorted[i]));
++localIndex;
}
return startIndex + localIndex;
}
private int addOffHand(int startIndex) {
// No off-hand?
if (!Inventory.EQUIPMENT_SLOT_MAPPING.containsValue(EquipmentSlot.OFFHAND)) {
return startIndex;
}
slots.set(startIndex, new ContentOffHand(owner));
return startIndex + 1;
}
private int addCrafting(int startIndex, boolean pretty) {
int listSize = owner.inventoryMenu.getCraftSlots().getContents().size();
pretty &= listSize == 4;
for (int localIndex = 0; localIndex < listSize; ++localIndex) {
// Pretty display is a 2x2 rather than linear.
// If index is in top row, grid is not 2x2, or pretty is disabled, just use current index.
// Otherwise, subtract 2 and add 9 to start in the same position on the next row.
int modIndex = startIndex + (localIndex < 2 || !pretty ? localIndex : localIndex + 7);
slots.set(modIndex, new ContentCrafting(owner, localIndex));
}
if (pretty) {
slots.set(startIndex + 2, new ContentViewOnly(owner) {
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotViewOnly(container, slot, x, y) {
@Override
public ItemStack getOrDefault() {
return Placeholders.craftingOutput;
}
};
}
}
);
slots.set(startIndex + 11, getCraftingResult(owner));
}
return startIndex + listSize;
}
protected Content getCraftingResult(@NotNull ServerPlayer serverPlayer) {
return new ContentCraftingResult(serverPlayer);
}
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 abstract Component getTitle(ServerPlayer player, @Nullable OpenChestMenu<?> menu);
@Override
public ServerPlayer getOwnerHandle() {
return owner;
}
@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 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 @NotNull ItemStack getItem(int index) {
return slots.get(index).get();
}
@Override
public @NotNull ItemStack removeItem(int index, int amount) {
return slots.get(index).removePartial(amount);
}
@Override
public @NotNull ItemStack removeItemNoUpdate(int index) {
return slots.get(index).remove();
}
@Override
public void setItem(int index, @NotNull 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(@NotNull Player player) {
return true;
}
@Override
public @NotNull 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(@NotNull CraftHumanEntity viewer) {
transaction.add(viewer);
}
@Override
public void onClose(@NotNull CraftHumanEntity viewer) {
transaction.remove(viewer);
}
@Override
public @NotNull List<HumanEntity> getViewers() {
return transaction;
}
@Override
public @NotNull 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);
}
public @Nullable OpenChestMenu<?> createMenu(Player player, int i, boolean viewOnly) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenInventoryMenu(this, serverPlayer, i, viewOnly);
}
return null;
}
}
@@ -2,16 +2,17 @@ package com.lishid.openinv.internal.common.container;
import com.lishid.openinv.internal.ISpecialEnderChest;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import com.lishid.openinv.internal.common.container.menu.OpenEnderChestMenu;
import com.lishid.openinv.internal.common.player.PlayerManager;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
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.entity.player.StackedItemContents;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.StackedContentsCompatible;
import net.minecraft.world.item.ItemStack;
import org.bukkit.Location;
@@ -177,13 +178,20 @@ public class OpenEnderChest implements Container, StackedContentsCompatible, Int
}
}
public Component getTitle() {
return Component.translatableWithFallback("openinv.container.enderchest.prefix", "", owner.getName())
public Component getTitle(@Nullable OpenChestMenu<?> menu) {
MutableComponent component;
if (menu != null && menu.isViewOnly()) {
component = Component.translatableWithFallback("openinv.container.enderchest.viewonly", "[RO] ");
} else {
component = Component.translatableWithFallback("openinv.container.enderchest.editable", "");
}
return component
.append(Component.translatableWithFallback("openinv.container.enderchest.prefix", "", owner.getName()))
.append(Component.translatable("container.enderchest"))
.append(Component.translatableWithFallback("openinv.container.enderchest.suffix", " - %s", owner.getName()));
}
public @Nullable AbstractContainerMenu createMenu(Player player, int i, boolean viewOnly) {
public @Nullable OpenChestMenu<?> createMenu(Player player, int i, boolean viewOnly) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenEnderChestMenu(this, serverPlayer, i, viewOnly);
}
@@ -1,223 +1,44 @@
package com.lishid.openinv.internal.common.container;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.bukkit.OpenPlayerInventory;
import com.lishid.openinv.internal.common.container.menu.OpenInventoryMenu;
import com.lishid.openinv.internal.common.container.slot.Content;
import com.lishid.openinv.internal.common.container.slot.ContentCrafting;
import com.lishid.openinv.internal.common.container.slot.ContentCraftingResult;
import com.lishid.openinv.internal.common.container.slot.ContentCursor;
import com.lishid.openinv.internal.common.container.slot.ContentDrop;
import com.lishid.openinv.internal.common.container.slot.ContentEquipment;
import com.lishid.openinv.internal.common.container.slot.ContentList;
import com.lishid.openinv.internal.common.container.slot.ContentOffHand;
import com.lishid.openinv.internal.common.container.slot.ContentViewOnly;
import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
import com.lishid.openinv.internal.common.container.slot.placeholder.Placeholders;
import com.lishid.openinv.internal.common.player.PlayerManager;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import net.minecraft.ChatFormatting;
import net.minecraft.core.NonNullList;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.FontDescription;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.Location;
import org.bukkit.craftbukkit.entity.CraftHumanEntity;
import org.bukkit.craftbukkit.inventory.CraftInventory;
import org.bukkit.entity.HumanEntity;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
public class OpenInventory extends BaseOpenInventory {
public class OpenInventory implements Container, InternalOwned<ServerPlayer>, ISpecialPlayerInventory {
protected final List<Content> slots;
private final int size;
protected ServerPlayer owner;
private int maxStackSize = 99;
protected 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();
public OpenInventory(@NotNull Player bukkitPlayer) {
super(bukkitPlayer);
}
protected void setupSlots() {
// Top of inventory: Regular contents.
int nextIndex = addMainInventory();
// If inventory is expected size, we can arrange slots to be pretty.
Inventory ownerInv = owner.getInventory();
if (ownerInv.getNonEquipmentItems().size() == 36
&& owner.inventoryMenu.getCraftSlots().getContainerSize() == 4
&& (Inventory.EQUIPMENT_SLOT_MAPPING.size() == 5 || Inventory.EQUIPMENT_SLOT_MAPPING.size() == 7)) {
// 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().getNonEquipmentItems().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().getNonEquipmentItems();
}
}
);
}
return listSize;
}
private int addArmor(int startIndex) {
// Armor slots go bottom to top; boots are first and helmet is last.
// Since we have to display horizontally due to space restrictions,
// making the left side the "top" is more user-friendly.
EquipmentSlot[] sorted = Inventory.EQUIPMENT_SLOT_MAPPING.int2ObjectEntrySet()
.stream()
.sorted(Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey))
.map(Map.Entry::getValue)
.toArray(EquipmentSlot[]::new);
int localIndex = 0;
for (int i = sorted.length - 1; i >= 0; --i) {
// Skip off-hand, handled separately. Also skip non-player slots.
if (sorted[i].getType() != EquipmentSlot.Type.HUMANOID_ARMOR) {
continue;
}
slots.set(startIndex + localIndex, new ContentEquipment(owner, sorted[i]));
++localIndex;
}
return startIndex + localIndex;
}
private int addOffHand(int startIndex) {
// No off-hand?
if (!Inventory.EQUIPMENT_SLOT_MAPPING.containsValue(EquipmentSlot.OFFHAND)) {
return startIndex;
}
slots.set(startIndex, new ContentOffHand(owner));
return startIndex + 1;
}
private int addCrafting(int startIndex, boolean pretty) {
int listSize = owner.inventoryMenu.getCraftSlots().getContents().size();
pretty &= listSize == 4;
for (int localIndex = 0; localIndex < listSize; ++localIndex) {
// Pretty display is a 2x2 rather than linear.
// If index is in top row, grid is not 2x2, or pretty is disabled, just use current index.
// Otherwise, subtract 2 and add 9 to start in the same position on the next row.
int modIndex = startIndex + (localIndex < 2 || !pretty ? localIndex : localIndex + 7);
slots.set(modIndex, new ContentCrafting(owner, localIndex));
}
if (pretty) {
slots.set(startIndex + 2, new ContentViewOnly(owner) {
@Override
public Slot asSlot(Container container, int slot, int x, int y) {
return new SlotViewOnly(container, slot, x, y) {
@Override
public ItemStack getOrDefault() {
return Placeholders.craftingOutput;
}
};
}
}
);
slots.set(startIndex + 11, getCraftingResult(owner));
}
return startIndex + listSize;
}
protected Content getCraftingResult(@NotNull ServerPlayer serverPlayer) {
return new ContentCraftingResult(serverPlayer);
}
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 @NotNull Component getTitle(@Nullable ServerPlayer viewer) {
@Override
public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu<?> menu) {
MutableComponent component = Component.empty();
// Prefix for use with custom bitmap image fonts.
if (owner.equals(viewer)) {
component.append(
Component.translatableWithFallback("openinv.container.inventory.self", "")
.withStyle(style -> style
.withFont(ResourceLocation.parse("openinv:font/inventory"))
.withFont(new FontDescription.Resource(Identifier.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"))
.withFont(new FontDescription.Resource(Identifier.parse("openinv:font/inventory")))
.withColor(ChatFormatting.WHITE)));
}
if (menu != null && menu.isViewOnly()) {
component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] "));
} else {
component.append(Component.translatableWithFallback("openinv.container.inventory.editable", ""));
}
// Normal title: "Inventory - OwnerName"
component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName()))
.append(Component.translatable("container.inventory"))
@@ -225,135 +46,4 @@ public class OpenInventory implements Container, InternalOwned<ServerPlayer>, IS
return component;
}
@Override
public ServerPlayer getOwnerHandle() {
return owner;
}
@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 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 @NotNull ItemStack getItem(int index) {
return slots.get(index).get();
}
@Override
public @NotNull ItemStack removeItem(int index, int amount) {
return slots.get(index).removePartial(amount);
}
@Override
public @NotNull ItemStack removeItemNoUpdate(int index) {
return slots.get(index).remove();
}
@Override
public void setItem(int index, @NotNull 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(@NotNull Player player) {
return true;
}
@Override
public @NotNull 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(@NotNull CraftHumanEntity viewer) {
transaction.add(viewer);
}
@Override
public void onClose(@NotNull CraftHumanEntity viewer) {
transaction.remove(viewer);
}
@Override
public @NotNull List<HumanEntity> getViewers() {
return transaction;
}
@Override
public @NotNull 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);
}
public @Nullable AbstractContainerMenu createMenu(Player player, int i, boolean viewOnly) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenInventoryMenu(this, serverPlayer, i, viewOnly);
}
return null;
}
}
@@ -1,7 +1,7 @@
package com.lishid.openinv.internal.common.container.bukkit;
import com.google.common.base.Preconditions;
import com.lishid.openinv.internal.common.container.OpenInventory;
import com.lishid.openinv.internal.common.container.BaseOpenInventory;
import net.minecraft.core.NonNullList;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
@@ -18,13 +18,13 @@ import java.util.List;
public class OpenPlayerInventory extends CraftInventory implements PlayerInventory {
public OpenPlayerInventory(@NotNull OpenInventory inventory) {
public OpenPlayerInventory(@NotNull BaseOpenInventory inventory) {
super(inventory);
}
@Override
public @NotNull OpenInventory getInventory() {
return (OpenInventory) super.getInventory();
public @NotNull BaseOpenInventory getInventory() {
return (BaseOpenInventory) super.getInventory();
}
@Override
@@ -1,6 +1,6 @@
package com.lishid.openinv.internal.common.container.bukkit;
import com.lishid.openinv.internal.common.container.OpenInventory;
import com.lishid.openinv.internal.common.container.BaseOpenInventory;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
@@ -8,7 +8,7 @@ public class OpenPlayerInventorySelf extends OpenPlayerInventory {
private final int offset;
public OpenPlayerInventorySelf(@NotNull OpenInventory inventory, int offset) {
public OpenPlayerInventorySelf(@NotNull BaseOpenInventory inventory, int offset) {
super(inventory);
this.offset = offset;
}
@@ -1,26 +1,16 @@
package com.lishid.openinv.internal.common.container.menu;
import com.google.common.base.Suppliers;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.bukkit.OpenDummyInventory;
import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder;
import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.network.HashedStack;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.inventory.ContainerSynchronizer;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.RemoteSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.inventory.CraftInventoryView;
@@ -30,14 +20,9 @@ import org.bukkit.inventory.InventoryView;
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}.
* An extension of {@link AbstractContainerMenu} storing and managing data common to all special inventories.
*/
@SuppressWarnings("HidingField") // Revisit when removing 1.21.4 support
public abstract class OpenChestMenu<T extends Container & ISpecialInventory & InternalOwned<ServerPlayer>>
extends AbstractContainerMenu {
@@ -49,13 +34,6 @@ public abstract class OpenChestMenu<T extends Container & ISpecialInventory & In
protected final boolean ownContainer;
protected final int topSize;
private CraftInventoryView<OpenChestMenu<T>, Inventory> bukkitEntity;
// Syncher fields
protected @Nullable ContainerSynchronizer synchronizer;
protected final List<DataSlot> dataSlots = new ArrayList<>();
protected final IntList remoteDataSlots = new IntArrayList();
protected final List<ContainerListener> containerListeners = new ArrayList<>();
private RemoteSlot remoteCarried = RemoteSlot.PLACEHOLDER;
protected boolean suppressRemoteUpdates;
protected OpenChestMenu(
@NotNull MenuType<ChestMenu> type,
@@ -125,6 +103,7 @@ public abstract class OpenChestMenu<T extends Container & ISpecialInventory & In
};
}
protected void preSlotSetup() {
}
@@ -136,6 +115,9 @@ public abstract class OpenChestMenu<T extends Container & ISpecialInventory & In
return slot;
}
public boolean isViewOnly() {
return viewOnly;
}
@Override
public final @NotNull CraftInventoryView<OpenChestMenu<T>, Inventory> getBukkitView() {
@@ -303,188 +285,4 @@ public abstract class OpenChestMenu<T extends Container & ISpecialInventory & In
return true;
}
// Overrides from here on are purely to modify the sync process to send placeholder items.
@Override
protected @NotNull Slot addSlot(@NotNull Slot slot) {
slot.index = this.slots.size();
this.slots.add(slot);
this.lastSlots.add(ItemStack.EMPTY);
this.remoteSlots.add(this.synchronizer != null ? this.synchronizer.createSlot() : RemoteSlot.PLACEHOLDER);
return slot;
}
@Override
protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) {
this.dataSlots.add(dataSlot);
this.remoteDataSlots.add(0);
return dataSlot;
}
@Override
protected void addDataSlots(ContainerData containerData) {
for (int i = 0; i < containerData.getCount(); i++) {
this.addDataSlot(DataSlot.forContainer(containerData, i));
}
}
@Override
public void addSlotListener(@NotNull ContainerListener containerListener) {
if (!this.containerListeners.contains(containerListener)) {
this.containerListeners.add(containerListener);
this.broadcastChanges();
}
}
@Override
public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) {
this.synchronizer = containerSynchronizer;
this.remoteCarried = synchronizer.createSlot();
this.remoteSlots.replaceAll(slot -> synchronizer.createSlot());
this.sendAllDataToRemote();
}
@Override
public void sendAllDataToRemote() {
List<ItemStack> contentsCopy = new ArrayList<>();
for (int index = 0; index < slots.size(); ++index) {
Slot slot = slots.get(index);
ItemStack itemStack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
contentsCopy.add(itemStack);
this.remoteSlots.get(index).force(itemStack);
}
remoteCarried.force(getCarried());
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, contentsCopy, this.getCarried().copy(), this.remoteDataSlots.toIntArray());
}
}
@Override
public void broadcastCarriedItem() {
ItemStack carried = this.getCarried();
this.remoteCarried.force(carried);
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, carried.copy());
}
}
@Override
public void removeSlotListener(@NotNull ContainerListener containerListener) {
this.containerListeners.remove(containerListener);
}
@Override
public void broadcastChanges() {
for (int index = 0; index < this.slots.size(); ++index) {
Slot slot = this.slots.get(index);
ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
Supplier<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) {
RemoteSlot slot = this.remoteSlots.get(i);
if (!slot.matches(itemStack)) {
slot.force(itemStack);
if (this.synchronizer != null) {
this.synchronizer.sendSlotChange(this, i, supplier.get());
}
}
}
}
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 carried = this.getCarried();
if (!this.remoteCarried.matches(carried)) {
this.remoteCarried.force(carried);
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, carried.copy());
}
}
}
}
@Override
public void setRemoteCarried(@NotNull HashedStack stack) {
this.remoteCarried.receive(stack);
}
@Override
public void suppressRemoteUpdates() {
this.suppressRemoteUpdates = true;
}
@Override
public void resumeRemoteUpdates() {
this.suppressRemoteUpdates = false;
}
}
@@ -7,7 +7,7 @@ import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
public class OpenEnderChestMenu extends OpenChestMenu<OpenEnderChest> {
public class OpenEnderChestMenu extends OpenSyncMenu<OpenEnderChest> {
public OpenEnderChestMenu(
@NotNull OpenEnderChest enderChest,
@@ -1,7 +1,7 @@
package com.lishid.openinv.internal.common.container.menu;
import com.google.common.base.Preconditions;
import com.lishid.openinv.internal.common.container.OpenInventory;
import com.lishid.openinv.internal.common.container.BaseOpenInventory;
import com.lishid.openinv.internal.common.container.bukkit.OpenDummyPlayerInventory;
import com.lishid.openinv.internal.common.container.bukkit.OpenPlayerInventorySelf;
import com.lishid.openinv.internal.common.container.slot.ContentDrop;
@@ -23,15 +23,15 @@ import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class OpenInventoryMenu extends OpenChestMenu<OpenInventory> {
public class OpenInventoryMenu extends OpenSyncMenu<BaseOpenInventory> {
private int offset;
public OpenInventoryMenu(OpenInventory inventory, ServerPlayer viewer, int i, boolean viewOnly) {
public OpenInventoryMenu(BaseOpenInventory inventory, ServerPlayer viewer, int i, boolean viewOnly) {
super(getMenuType(inventory, viewer), i, inventory, viewer, viewOnly);
}
private static MenuType<ChestMenu> getMenuType(OpenInventory inventory, ServerPlayer viewer) {
private static MenuType<ChestMenu> getMenuType(BaseOpenInventory inventory, ServerPlayer viewer) {
int size = inventory.getContainerSize();
// Disallow duplicate access to own main inventory contents.
if (inventory.getOwnerHandle().equals(viewer)) {
@@ -99,7 +99,7 @@ public class OpenInventoryMenu extends OpenChestMenu<OpenInventory> {
}
@Override
protected @NotNull CraftInventoryView<OpenChestMenu<OpenInventory>, Inventory> createBukkitEntity() {
protected @NotNull CraftInventoryView<OpenChestMenu<BaseOpenInventory>, Inventory> createBukkitEntity() {
org.bukkit.inventory.Inventory bukkitInventory;
if (viewOnly) {
bukkitInventory = new OpenDummyPlayerInventory(container);
@@ -0,0 +1,251 @@
package com.lishid.openinv.internal.common.container.menu;
import com.google.common.base.Suppliers;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.network.HashedStack;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.inventory.ContainerSynchronizer;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.RemoteSlot;
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 OpenChestMenu} that supports {@link SlotPlaceholder placeholders}.
*/
@SuppressWarnings("HidingField") // Revisit when removing 1.21.4 support
public abstract class OpenSyncMenu<T extends Container & ISpecialInventory & InternalOwned<ServerPlayer>>
extends OpenChestMenu<T> {
// Syncher fields
protected @Nullable ContainerSynchronizer synchronizer;
protected final List<DataSlot> dataSlots = new ArrayList<>();
protected final IntList remoteDataSlots = new IntArrayList();
protected final List<ContainerListener> containerListeners = new ArrayList<>();
private RemoteSlot remoteCarried = RemoteSlot.PLACEHOLDER;
protected boolean suppressRemoteUpdates;
protected OpenSyncMenu(
@NotNull MenuType<ChestMenu> type,
int containerCounter,
@NotNull T container,
@NotNull ServerPlayer viewer,
boolean viewOnly
) {
super(type, containerCounter, container, viewer, viewOnly);
}
// Overrides from here on are purely to modify the sync process to send placeholder items.
@Override
protected @NotNull Slot addSlot(@NotNull Slot slot) {
slot.index = this.slots.size();
this.slots.add(slot);
this.lastSlots.add(ItemStack.EMPTY);
this.remoteSlots.add(this.synchronizer != null ? this.synchronizer.createSlot() : RemoteSlot.PLACEHOLDER);
return slot;
}
@Override
protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) {
this.dataSlots.add(dataSlot);
this.remoteDataSlots.add(0);
return dataSlot;
}
@Override
protected void addDataSlots(ContainerData containerData) {
for (int i = 0; i < containerData.getCount(); i++) {
this.addDataSlot(DataSlot.forContainer(containerData, i));
}
}
@Override
public void addSlotListener(@NotNull ContainerListener containerListener) {
if (!this.containerListeners.contains(containerListener)) {
this.containerListeners.add(containerListener);
this.broadcastChanges();
}
}
@Override
public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) {
this.synchronizer = containerSynchronizer;
this.remoteCarried = synchronizer.createSlot();
this.remoteSlots.replaceAll(slot -> synchronizer.createSlot());
this.sendAllDataToRemote();
}
@Override
public void sendAllDataToRemote() {
List<ItemStack> contentsCopy = new ArrayList<>();
for (int index = 0; index < slots.size(); ++index) {
Slot slot = slots.get(index);
ItemStack itemStack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
contentsCopy.add(itemStack);
this.remoteSlots.get(index).force(itemStack);
}
remoteCarried.force(getCarried());
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, contentsCopy, this.getCarried().copy(), this.remoteDataSlots.toIntArray());
}
}
@Override
public void forceSlot(@NotNull Container container, int slot) {
int slotsIndex = this.findSlot(container, slot).orElse(-1);
if (slotsIndex != -1) {
ItemStack item = this.slots.get(slotsIndex).getItem();
this.remoteSlots.get(slotsIndex).force(item);
if (this.synchronizer != null) {
this.synchronizer.sendSlotChange(this, slotsIndex, item.copy());
}
}
}
@Override
public void broadcastCarriedItem() {
ItemStack carried = this.getCarried();
this.remoteCarried.force(carried);
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, carried.copy());
}
}
@Override
public void removeSlotListener(@NotNull ContainerListener containerListener) {
this.containerListeners.remove(containerListener);
}
@Override
public void broadcastChanges() {
for (int index = 0; index < this.slots.size(); ++index) {
Slot slot = this.slots.get(index);
ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
Supplier<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);
}
}
@Override
public void triggerSlotListeners(int index, @NotNull ItemStack itemStack, @NotNull 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);
}
}
}
@Override
public void synchronizeSlotToRemote(int i, @NotNull ItemStack itemStack, @NotNull Supplier<ItemStack> supplier) {
if (!this.suppressRemoteUpdates) {
RemoteSlot slot = this.remoteSlots.get(i);
if (!slot.matches(itemStack)) {
slot.force(itemStack);
if (this.synchronizer != null) {
this.synchronizer.sendSlotChange(this, i, supplier.get());
}
}
}
}
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 carried = this.getCarried();
if (!this.remoteCarried.matches(carried)) {
this.remoteCarried.force(carried);
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, carried.copy());
}
}
}
}
@Override
public void setRemoteCarried(@NotNull HashedStack stack) {
this.remoteCarried.receive(stack);
}
@Override
public void suppressRemoteUpdates() {
this.suppressRemoteUpdates = true;
}
@Override
public void resumeRemoteUpdates() {
this.suppressRemoteUpdates = false;
}
}
@@ -1,10 +1,13 @@
package com.lishid.openinv.internal.common.player;
import com.lishid.openinv.event.OpenEvents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NumericTag;
import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.level.storage.PlayerDataStorage;
import net.minecraft.world.level.storage.ValueOutput;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.entity.CraftPlayer;
@@ -13,14 +16,18 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
public abstract class BaseOpenPlayer extends CraftPlayer {
/**
* List of tags to always reset when saving.
* List of tags to always reset when saving. These are items that do not get written
* if unset or empty, resulting in older values not being clobbered appropriately.
*
* @see net.minecraft.world.entity.Entity#saveWithoutId(ValueOutput)
* @see net.minecraft.world.entity.Entity#saveWithoutId(ValueOutput, boolean, boolean, boolean)
* @see net.minecraft.server.level.ServerPlayer#addAdditionalSaveData(ValueOutput)
* @see net.minecraft.world.entity.player.Player#addAdditionalSaveData(ValueOutput)
* @see net.minecraft.world.entity.LivingEntity#addAdditionalSaveData(ValueOutput)
@@ -36,11 +43,12 @@ public abstract class BaseOpenPlayer extends CraftPlayer {
"TicksFrozen",
"HasVisualFire",
"Tags",
"data",
"Passengers",
// ServerPlayer#addAdditionalSaveData(CompoundTag)
// Intentional omissions to prevent mount loss: Attach, Entity, and RootVehicle
"warden_spawn_tracker",
"entered_nether_pos", // Replaces enteredNetherPosition
"warden_spawn_tracker", // No longer needed as of 1.21.11
"entered_nether_pos", // Replaces enteredNetherPosition as of 1.21.6
"enteredNetherPosition",
"respawn", // Replaces SpawnXyz fields as of 1.21.6
"SpawnX",
@@ -51,9 +59,9 @@ public abstract class BaseOpenPlayer extends CraftPlayer {
"SpawnDimension",
"raid_omen_position",
"ender_pearls",
// Player#addAdditionalSaveData(CompoundTag)
"ShoulderEntityLeft",
"ShoulderEntityRight",
// Player#addAdditionalSaveData(CompoundTag)
"LastDeathLocation",
"current_explosion_impact_pos",
// LivingEntity#addAdditionalSaveData(CompoundTag)
@@ -84,7 +92,40 @@ public abstract class BaseOpenPlayer extends CraftPlayer {
}
@Override
public abstract void saveData();
public void saveData() {
if (OpenEvents.saveCancelled(this)) {
return;
}
trySave(this.getHandle());
}
protected abstract void trySave(ServerPlayer player);
protected void saveSafe(
@NotNull ServerPlayer player,
@Nullable CompoundTag oldData,
@NotNull CompoundTag playerData,
@NotNull PlayerDataStorage worldNbtStorage
) throws IOException {
// Revert certain special data values when offline.
revertSpecialValues(playerData, oldData);
Path playerDataDir = worldNbtStorage.getPlayerDir().toPath();
Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
NbtIo.writeCompressed(playerData, tempFile);
Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
safeReplaceFile(dataFile, tempFile, backupFile);
}
protected void safeReplaceFile(
@NotNull Path dataFile,
@NotNull Path tempFile,
@NotNull Path backupFile
) {
net.minecraft.util.Util.safeReplaceFile(dataFile, tempFile, backupFile);
}
@Contract("null -> new")
protected @NotNull CompoundTag getWritableTag(@Nullable CompoundTag oldData) {
@@ -105,7 +146,11 @@ public abstract class BaseOpenPlayer extends CraftPlayer {
return oldData;
}
protected void revertSpecialValues(@NotNull CompoundTag newData, @NotNull CompoundTag oldData) {
protected void revertSpecialValues(@NotNull CompoundTag newData, @Nullable CompoundTag oldData) {
if (oldData == null) {
return;
}
// Revert automatic updates to play timestamps.
copyValue(oldData, newData, "bukkit", "lastPlayed", NumericTag.class);
copyValue(oldData, newData, "Paper", "LastSeen", NumericTag.class);
@@ -152,12 +197,14 @@ public abstract class BaseOpenPlayer extends CraftPlayer {
@Nullable T data
) {
if (data == null) {
container.remove(key);
remove(container, key);
} else {
container.put(key, data);
}
}
protected abstract void remove(@NotNull CompoundTag tag, @NotNull String key);
public static boolean isConnected(@Nullable ServerGamePacketListenerImpl connection) {
return connection != null && !connection.isDisconnected();
}
@@ -1,21 +1,16 @@
package com.lishid.openinv.internal.common.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.server.level.ServerPlayer;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.level.storage.PlayerDataStorage;
import net.minecraft.world.level.storage.TagValueOutput;
import net.minecraft.world.level.storage.ValueOutput;
import org.bukkit.craftbukkit.CraftServer;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.nio.file.Files;
import java.nio.file.Path;
public class OpenPlayer extends BaseOpenPlayer {
protected OpenPlayer(
@@ -27,39 +22,29 @@ public class OpenPlayer extends BaseOpenPlayer {
}
@Override
public void saveData() {
if (OpenEvents.saveCancelled(this)) {
return;
}
ServerPlayer player = this.getHandle();
protected void trySave(ServerPlayer player) {
Logger logger = LogUtils.getLogger();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) {
PlayerDataStorage worldNBTStorage = server.getServer().getPlayerList().playerIo;
PlayerDataStorage worldNbtStorage = server.getServer().getPlayerList().playerIo;
CompoundTag oldData = isOnline()
? null
: worldNBTStorage.load(player.getName().getString(), player.getStringUUID(), scopedCollector).orElse(null);
: worldNbtStorage.load(player.nameAndId()).orElse(null);
CompoundTag playerData = getWritableTag(oldData);
ValueOutput valueOutput = TagValueOutput.createWrappingWithContext(scopedCollector, player.registryAccess(), playerData);
player.saveWithoutId(valueOutput);
if (oldData != null) {
// Revert certain special data values when offline.
revertSpecialValues(playerData, oldData);
}
Path playerDataDir = worldNBTStorage.getPlayerDir().toPath();
Path tempFile = Files.createTempFile(playerDataDir, player.getStringUUID() + "-", ".dat");
NbtIo.writeCompressed(playerData, tempFile);
Path dataFile = playerDataDir.resolve(player.getStringUUID() + ".dat");
Path backupFile = playerDataDir.resolve(player.getStringUUID() + ".dat_old");
Util.safeReplaceFile(dataFile, tempFile, backupFile);
saveSafe(player, oldData, playerData, worldNbtStorage);
} catch (Exception e) {
LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
}
}
@Override
protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
tag.remove(key);
}
}
@@ -1,11 +1,13 @@
package com.lishid.openinv.internal.common.player;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.common.container.BaseOpenInventory;
import com.lishid.openinv.internal.common.container.OpenEnderChest;
import com.lishid.openinv.internal.common.container.OpenInventory;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import com.lishid.openinv.util.JulLoggerAdapter;
import com.mojang.authlib.GameProfile;
import io.papermc.paper.adventure.PaperAdventure;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
@@ -18,6 +20,8 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.ChatVisiblity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.LevelData;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.level.storage.ValueInput;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
@@ -143,26 +147,30 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
// See CraftPlayer#loadData
try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) {
ValueInput loadedData = server.getPlayerList().playerIo.load(player, scopedCollector).orElse(null);
CompoundTag loadedData = server.getPlayerList().playerIo.load(player.nameAndId()).orElse(null);
if (loadedData == null) {
// Exceptions with loading are logged.
return false;
}
ValueInput valueInput = TagValueInput.create(scopedCollector, player.registryAccess(), loadedData);
// Read basic data into the player.
player.load(loadedData);
// Game type settings are loaded separately.
player.loadGameTypes(loadedData);
player.load(valueInput);
// World is not loaded by ServerPlayer#load(CompoundTag) on Paper.
parseWorld(server, player, loadedData);
parseWorld(server, player, valueInput);
}
return true;
}
private void parseWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player, @NotNull ValueInput loadedData) {
protected void parseWorld(
@NotNull MinecraftServer server,
@NotNull ServerPlayer player,
@NotNull ValueInput loadedData
) {
// See PlayerList#placeNewPlayer
World bukkitWorld;
Optional<Long> msbs = loadedData.getLong("WorldUUIDMost");
@@ -184,7 +192,8 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
ServerLevel level = server.getLevel(Level.OVERWORLD);
if (level != null) {
// Adjust player to default spawn (in keeping with Paper handling) when world not found.
player.snapTo(player.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), level.getSharedSpawnAngle(), 0.0F);
LevelData.RespawnData respawnData = level.levelData.getRespawnData();
player.snapTo(player.adjustSpawnLocation(level, respawnData.pos()).getBottomCenter(), respawnData.yaw(), 0.0F);
player.spawnIn(level);
} else {
logger.warning("Tried to load player with invalid world when no fallback was available!");
@@ -208,16 +217,7 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
if (nmsPlayer.getBukkitEntity() instanceof BaseOpenPlayer openPlayer) {
return openPlayer;
}
MinecraftServer server = nmsPlayer.getServer();
if (server == null) {
if (!(Bukkit.getServer() instanceof CraftServer craftServer)) {
logger.warning(() ->
"Unable to inject ServerPlayer, certain player data may be lost when saving! Server is not a CraftServer: "
+ Bukkit.getServer().getClass().getName());
return player;
}
server = craftServer.getServer();
}
MinecraftServer server = nmsPlayer.level().getServer();
injectPlayer(server, nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
@@ -242,14 +242,14 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
}
// See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider)
AbstractContainerMenu menu;
OpenChestMenu<?> menu;
Component title;
if (inventory instanceof OpenInventory playerInv) {
if (inventory instanceof BaseOpenInventory playerInv) {
menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly);
title = playerInv.getTitle(player);
title = playerInv.getTitle(player, menu);
} else if (inventory instanceof OpenEnderChest enderChest) {
menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly);
title = enderChest.getTitle();
title = enderChest.getTitle(menu);
} else {
return null;
}
@@ -265,10 +265,10 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
menu.setTitle(title);
var pair = CraftEventFactory.callInventoryOpenEventWithTitle(player, menu);
menu = pair.getSecond();
AbstractContainerMenu opened = pair.getSecond();
// Menu is null if event is cancelled.
if (menu == null) {
if (opened == null) {
return null;
}
@@ -277,11 +277,11 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
title = PaperAdventure.asVanilla(newTitle);
}
player.containerMenu = menu;
player.connection.send(new ClientboundOpenScreenPacket(menu.containerId, menu.getType(), title));
player.initMenu(menu);
player.containerMenu = opened;
player.connection.send(new ClientboundOpenScreenPacket(opened.containerId, opened.getType(), title));
player.initMenu(opened);
return menu.getBukkitView();
return opened.getBukkitView();
}
}
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
implementation(project(":openinvadapterpaper1_21_8"))
implementation(project(":openinvadapterpaper1_21_5"))
implementation(project(":openinvadapterpaper1_21_4"))
implementation(project(":openinvadapterpaper1_21_3"))
+26
View File
@@ -0,0 +1,26 @@
plugins {
`openinv-base`
alias(libs.plugins.paperweight)
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
val paper = candidates.firstOrNull {
it.id.let { id ->
id is ModuleComponentIdentifier && id.module == "paper-api"
}
}
if (paper != null) {
select(paper)
}
because("module is written for Paper servers")
}
}
dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
paperweight.paperDevBundle("1.21.10-R0.1-SNAPSHOT")
}
@@ -0,0 +1,31 @@
package com.lishid.openinv.internal.paper1_21_10;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.internal.paper1_21_10.container.OpenInventory;
import com.lishid.openinv.internal.paper1_21_10.player.PlayerManager;
import com.lishid.openinv.util.lang.LanguageManager;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Logger;
public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor {
private final @NotNull PlayerManager manager;
public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
super(logger, lang);
manager = new PlayerManager(logger);
}
@Override
public @NotNull PlayerManager getPlayerManager() {
return manager;
}
@Override
public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
return new OpenInventory(player);
}
}
@@ -0,0 +1,50 @@
package com.lishid.openinv.internal.paper1_21_10.container;
import com.lishid.openinv.internal.common.container.BaseOpenInventory;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.FontDescription;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class OpenInventory extends BaseOpenInventory {
public OpenInventory(@NotNull Player bukkitPlayer) {
super(bukkitPlayer);
}
@Override
public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu<?> menu) {
MutableComponent component = Component.empty();
// Prefix for use with custom bitmap image fonts.
if (owner.equals(viewer)) {
component.append(
Component.translatableWithFallback("openinv.container.inventory.self", "")
.withStyle(style -> style
.withFont(new FontDescription.Resource(ResourceLocation.parse("openinv:font/inventory")))
.withColor(ChatFormatting.WHITE)));
} else {
component.append(
Component.translatableWithFallback("openinv.container.inventory.other", "")
.withStyle(style -> style
.withFont(new FontDescription.Resource(ResourceLocation.parse("openinv:font/inventory")))
.withColor(ChatFormatting.WHITE)));
}
if (menu != null && menu.isViewOnly()) {
component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] "));
} else {
component.append(Component.translatableWithFallback("openinv.container.inventory.editable", ""));
}
// Normal title: "Inventory - OwnerName"
component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName()))
.append(Component.translatable("container.inventory"))
.append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName()));
return component;
}
}
@@ -0,0 +1,35 @@
package com.lishid.openinv.internal.paper1_21_10.player;
import com.lishid.openinv.internal.common.player.PlayerManager;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.craftbukkit.CraftServer;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Path;
public class OpenPlayer extends com.lishid.openinv.internal.common.player.OpenPlayer {
protected OpenPlayer(
CraftServer server,
ServerPlayer entity,
PlayerManager manager
) {
super(server, entity, manager);
}
@Override
protected void safeReplaceFile(
@NotNull Path dataFile,
@NotNull Path tempFile,
@NotNull Path backupFile
) {
net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile);
}
@Override
protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
tag.remove(key);
}
}
@@ -0,0 +1,26 @@
package com.lishid.openinv.internal.paper1_21_10.player;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Logger;
public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager {
public PlayerManager(@NotNull Logger logger) {
super(logger);
}
@Override
protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(server.server, player, this));
}
}
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
implementation(project(":openinvadapterpaper1_21_8"))
implementation(project(":openinvadapterpaper1_21_5"))
implementation(project(":openinvadapterpaper1_21_4"))
+1
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
implementation(project(":openinvadapterpaper1_21_8"))
implementation(project(":openinvadapterpaper1_21_5"))
paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT")
@@ -14,7 +14,7 @@ public class OpenEnderChest extends com.lishid.openinv.internal.common.container
}
@Override
public @Nullable AbstractContainerMenu createMenu(
public @Nullable OpenEnderChestMenu createMenu(
net.minecraft.world.entity.player.Player player,
int i,
boolean viewOnly
@@ -1,5 +1,6 @@
package com.lishid.openinv.internal.paper1_21_4.container;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import com.lishid.openinv.internal.common.container.slot.ContentCrafting;
import com.lishid.openinv.internal.common.container.slot.ContentCursor;
import com.lishid.openinv.internal.common.container.slot.ContentDrop;
@@ -16,14 +17,13 @@ import net.minecraft.world.Container;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class OpenInventory extends com.lishid.openinv.internal.common.container.OpenInventory {
public class OpenInventory extends com.lishid.openinv.internal.paper1_21_8.container.OpenInventory {
public OpenInventory(@NotNull org.bukkit.entity.Player bukkitPlayer) {
super(bukkitPlayer);
@@ -181,7 +181,7 @@ public class OpenInventory extends com.lishid.openinv.internal.common.container.
}
@Override
public @Nullable AbstractContainerMenu createMenu(Player player, int i, boolean viewOnly) {
public @Nullable OpenChestMenu<?> createMenu(Player player, int i, boolean viewOnly) {
if (player instanceof ServerPlayer serverPlayer) {
return new OpenInventoryMenu(this, serverPlayer, i, viewOnly);
}
@@ -1,467 +0,0 @@
package com.lishid.openinv.internal.paper1_21_4.container.menu;
import com.google.common.base.Suppliers;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.bukkit.OpenDummyInventory;
import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder;
import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.ClickType;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.inventory.ContainerSynchronizer;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.inventory.CraftInventoryView;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* An extension of {@link AbstractContainerMenu} that supports {@link SlotPlaceholder placeholders}.
*/
@SuppressWarnings("HidingField")
public abstract class OpenChestMenu<T extends Container & ISpecialInventory & InternalOwned<ServerPlayer>>
extends AbstractContainerMenu {
protected static final int BOTTOM_INVENTORY_SIZE = 36;
protected final T container;
protected final ServerPlayer viewer;
protected final boolean viewOnly;
protected final boolean ownContainer;
protected final int topSize;
private CraftInventoryView<OpenChestMenu<T>, Inventory> bukkitEntity;
// Syncher fields
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 OpenChestMenu(
@NotNull MenuType<ChestMenu> type,
int containerCounter,
@NotNull T container,
@NotNull ServerPlayer viewer,
boolean viewOnly
) {
super(type, containerCounter);
this.container = container;
this.viewer = viewer;
this.viewOnly = viewOnly;
ownContainer = container.getOwnerHandle().equals(viewer);
topSize = getTopSize(viewer);
preSlotSetup();
int upperRows = topSize / 9;
// View's upper inventory - our container
for (int row = 0; row < upperRows; ++row) {
for (int col = 0; col < 9; ++col) {
// x and y for client purposes, but hey, we're thorough here.
// Adapted from net.minecraft.world.inventory.ChestMenu
int x = 8 + col * 18;
int y = 18 + row * 18;
int index = row * 9 + col;
// Guard against weird inventory sizes.
if (index >= container.getContainerSize()) {
addSlot(new SlotViewOnly(container, index, x, y));
continue;
}
Slot slot = getUpperSlot(index, x, y);
addSlot(slot);
}
}
// View's lower inventory - viewer inventory
int playerInvPad = (upperRows - 4) * 18;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + row * 18 + 103;
addSlot(new Slot(viewer.getInventory(), row * 9 + col + 9, x, y));
}
}
// Hotbar
for (int col = 0; col < 9; ++col) {
int x = 8 + col * 18;
int y = playerInvPad + 161;
addSlot(new Slot(viewer.getInventory(), col, x, y));
}
}
protected void preSlotSetup() {
}
protected @NotNull Slot getUpperSlot(int index, int x, int y) {
Slot slot = new Slot(container, index, x, y);
if (viewOnly) {
return SlotViewOnly.wrap(slot);
}
return slot;
}
@Override
public final @NotNull CraftInventoryView<OpenChestMenu<T>, Inventory> getBukkitView() {
if (bukkitEntity == null) {
bukkitEntity = createBukkitEntity();
}
return bukkitEntity;
}
protected @NotNull CraftInventoryView<OpenChestMenu<T>, Inventory> createBukkitEntity() {
Inventory top;
if (viewOnly) {
top = new OpenDummyInventory(container, container.getBukkitType());
} else {
top = container.getBukkitInventory();
}
return new CraftInventoryView<>(viewer.getBukkitEntity(), top, this) {
@Override
public @Nullable Inventory getInventory(int rawSlot) {
if (viewOnly) {
return null;
}
return super.getInventory(rawSlot);
}
@Override
public int convertSlot(int rawSlot) {
if (viewOnly) {
return InventoryView.OUTSIDE;
}
return super.convertSlot(rawSlot);
}
@Override
public @NotNull InventoryType.SlotType getSlotType(int slot) {
if (viewOnly) {
return InventoryType.SlotType.OUTSIDE;
}
return super.getSlotType(slot);
}
};
}
private int getTopSize(ServerPlayer viewer) {
MenuType<?> menuType = getType();
if (menuType == MenuType.GENERIC_9x1) {
return 9;
} else if (menuType == MenuType.GENERIC_9x2) {
return 18;
} else if (menuType == MenuType.GENERIC_9x3) {
return 27;
} else if (menuType == MenuType.GENERIC_9x4) {
return 36;
} else if (menuType == MenuType.GENERIC_9x5) {
return 45;
} else if (menuType == MenuType.GENERIC_9x6) {
return 54;
}
// This is a bit gross, but allows us a safe fallthrough.
return menuType.create(-1, viewer.getInventory()).slots.size() - BOTTOM_INVENTORY_SIZE;
}
/**
* 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 max = slot.getMaxStackSize(existing);
int existingCount = existing.getCount();
// If the stack is already full, we can't add more.
if (existingCount >= max) {
return false;
}
int total = existingCount + itemStack.getCount();
// If the existing item can accept the entirety of our item, we're done!
if (total <= max) {
itemStack.setCount(0);
existing.setCount(total);
slot.setChanged();
return true;
}
// Otherwise, add as many as we can.
itemStack.shrink(max - existingCount);
existing.setCount(max);
slot.setChanged();
return true;
}
@Override
public void clicked(int i, int j, @NotNull ClickType clickType, @NotNull Player player) {
if (viewOnly) {
if (clickType == ClickType.QUICK_CRAFT) {
sendAllDataToRemote();
}
return;
}
super.clicked(i, j, clickType, player);
}
@Override
public boolean stillValid(@NotNull Player player) {
return true;
}
// Overrides from here on are purely to modify the sync process to send placeholder items.
@Override
protected @NotNull Slot addSlot(@NotNull Slot slot) {
slot.index = this.slots.size();
this.slots.add(slot);
this.lastSlots.add(ItemStack.EMPTY);
this.remoteSlots.add(ItemStack.EMPTY);
return slot;
}
@Override
protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) {
this.dataSlots.add(dataSlot);
this.remoteDataSlots.add(0);
return dataSlot;
}
@Override
protected void addDataSlots(ContainerData containerData) {
for (int i = 0; i < containerData.getCount(); i++) {
this.addDataSlot(DataSlot.forContainer(containerData, i));
}
}
@Override
public void addSlotListener(@NotNull ContainerListener containerListener) {
if (!this.containerListeners.contains(containerListener)) {
this.containerListeners.add(containerListener);
this.broadcastChanges();
}
}
@Override
public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) {
this.synchronizer = containerSynchronizer;
this.sendAllDataToRemote();
}
@Override
public void sendAllDataToRemote() {
for (int index = 0; index < slots.size(); ++index) {
Slot slot = slots.get(index);
this.remoteSlots.set(index, (slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem()).copy());
}
remoteCarried = getCarried().copy();
for (int index = 0; index < this.dataSlots.size(); ++index) {
this.remoteDataSlots.set(index, this.dataSlots.get(index).get());
}
if (this.synchronizer != null) {
this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray());
}
}
@Override
public void broadcastCarriedItem() {
this.remoteCarried = this.getCarried().copy();
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, this.remoteCarried);
}
}
@Override
public void removeSlotListener(@NotNull ContainerListener containerListener) {
this.containerListeners.remove(containerListener);
}
@Override
public void broadcastChanges() {
for (int index = 0; index < this.slots.size(); ++index) {
Slot slot = this.slots.get(index);
ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
Supplier<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,13 +1,14 @@
package com.lishid.openinv.internal.paper1_21_4.container.menu;
import com.lishid.openinv.internal.common.container.OpenEnderChest;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
public class OpenEnderChestMenu extends OpenChestMenu<OpenEnderChest> {
public class OpenEnderChestMenu extends OpenSyncMenu<OpenEnderChest> {
public OpenEnderChestMenu(
@NotNull OpenEnderChest enderChest,
@@ -16,7 +17,7 @@ public class OpenEnderChestMenu extends OpenChestMenu<OpenEnderChest> {
boolean viewOnly
) {
super(
com.lishid.openinv.internal.common.container.menu.OpenChestMenu.getChestMenuType(enderChest.getContainerSize()),
OpenChestMenu.getChestMenuType(enderChest.getContainerSize()),
containerId,
enderChest,
viewer,
@@ -2,6 +2,7 @@ package com.lishid.openinv.internal.paper1_21_4.container.menu;
import com.google.common.base.Preconditions;
import com.lishid.openinv.internal.common.container.bukkit.OpenDummyPlayerInventory;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import com.lishid.openinv.internal.common.container.slot.ContentDrop;
import com.lishid.openinv.internal.common.container.slot.SlotViewOnly;
import com.lishid.openinv.internal.paper1_21_4.container.OpenInventory;
@@ -23,7 +24,7 @@ import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class OpenInventoryMenu extends OpenChestMenu<OpenInventory> {
public class OpenInventoryMenu extends OpenSyncMenu<OpenInventory> {
private int offset;
@@ -39,7 +40,7 @@ public class OpenInventoryMenu extends OpenChestMenu<OpenInventory> {
size = ((int) Math.ceil(size / 9.0)) * 9;
}
return com.lishid.openinv.internal.common.container.menu.OpenChestMenu.getChestMenuType(size);
return OpenChestMenu.getChestMenuType(size);
}
@Override
@@ -0,0 +1,229 @@
package com.lishid.openinv.internal.paper1_21_4.container.menu;
import com.google.common.base.Suppliers;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.InternalOwned;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import com.lishid.openinv.internal.common.container.slot.SlotPlaceholder;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.Container;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.inventory.ChestMenu;
import net.minecraft.world.inventory.ContainerData;
import net.minecraft.world.inventory.ContainerListener;
import net.minecraft.world.inventory.ContainerSynchronizer;
import net.minecraft.world.inventory.DataSlot;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* An extension of {@link AbstractContainerMenu} that supports {@link SlotPlaceholder placeholders}.
*/
@SuppressWarnings("HidingField")
public abstract class OpenSyncMenu<T extends Container & ISpecialInventory & InternalOwned<ServerPlayer>>
extends OpenChestMenu<T> {
// Syncher fields
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 OpenSyncMenu(
@NotNull MenuType<ChestMenu> type,
int containerCounter,
@NotNull T container,
@NotNull ServerPlayer viewer,
boolean viewOnly
) {
super(type, containerCounter, container, viewer, viewOnly);
}
// Overrides from here on are purely to modify the sync process to send placeholder items.
@Override
protected @NotNull Slot addSlot(@NotNull Slot slot) {
slot.index = this.slots.size();
this.slots.add(slot);
this.lastSlots.add(ItemStack.EMPTY);
this.remoteSlots.add(ItemStack.EMPTY);
return slot;
}
@Override
protected @NotNull DataSlot addDataSlot(@NotNull DataSlot dataSlot) {
this.dataSlots.add(dataSlot);
this.remoteDataSlots.add(0);
return dataSlot;
}
@Override
protected void addDataSlots(ContainerData containerData) {
for (int i = 0; i < containerData.getCount(); i++) {
this.addDataSlot(DataSlot.forContainer(containerData, i));
}
}
@Override
public void addSlotListener(@NotNull ContainerListener containerListener) {
if (!this.containerListeners.contains(containerListener)) {
this.containerListeners.add(containerListener);
this.broadcastChanges();
}
}
@Override
public void setSynchronizer(@NotNull ContainerSynchronizer containerSynchronizer) {
this.synchronizer = containerSynchronizer;
this.sendAllDataToRemote();
}
@Override
public void sendAllDataToRemote() {
for (int index = 0; index < slots.size(); ++index) {
Slot slot = slots.get(index);
this.remoteSlots.set(index, (slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem()).copy());
}
remoteCarried = getCarried().copy();
for (int index = 0; index < this.dataSlots.size(); ++index) {
this.remoteDataSlots.set(index, this.dataSlots.get(index).get());
}
if (this.synchronizer != null) {
this.synchronizer.sendInitialData(this, this.remoteSlots, this.remoteCarried, this.remoteDataSlots.toIntArray());
}
}
@Override
public void broadcastCarriedItem() {
this.remoteCarried = this.getCarried().copy();
if (this.synchronizer != null) {
this.synchronizer.sendCarriedChange(this, this.remoteCarried);
}
}
@Override
public void removeSlotListener(@NotNull ContainerListener containerListener) {
this.containerListeners.remove(containerListener);
}
@Override
public void broadcastChanges() {
for (int index = 0; index < this.slots.size(); ++index) {
Slot slot = this.slots.get(index);
ItemStack itemstack = slot instanceof SlotPlaceholder placeholder ? placeholder.getOrDefault() : slot.getItem();
Supplier<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
View File
@@ -21,6 +21,7 @@ dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
implementation(project(":openinvadapterpaper1_21_8"))
paperweight.paperDevBundle("1.21.5-R0.1-SNAPSHOT")
}
@@ -9,7 +9,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.logging.Level;
import java.util.logging.Logger;
public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor {
public class InternalAccessor extends com.lishid.openinv.internal.paper1_21_8.InternalAccessor {
private final @NotNull PlayerManager manager;
@@ -1,16 +1,13 @@
package com.lishid.openinv.internal.paper1_21_5.player;
import com.lishid.openinv.event.OpenEvents;
import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
import com.mojang.logging.LogUtils;
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.bukkit.craftbukkit.CraftServer;
import org.jetbrains.annotations.NotNull;
import java.nio.file.Files;
import java.nio.file.Path;
public class OpenPlayer extends BaseOpenPlayer {
@@ -20,12 +17,7 @@ public class OpenPlayer extends BaseOpenPlayer {
}
@Override
public void saveData() {
if (OpenEvents.saveCancelled(this)) {
return;
}
ServerPlayer player = this.getHandle();
protected void trySave(ServerPlayer player) {
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try {
PlayerDataStorage worldNBTStorage = player.server.getPlayerList().playerIo;
@@ -35,20 +27,20 @@ public class OpenPlayer extends BaseOpenPlayer {
playerData = player.saveWithoutId(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);
saveSafe(player, oldData, playerData, worldNBTStorage);
} catch (Exception e) {
LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
}
}
@Override
protected void safeReplaceFile(@NotNull Path dataFile, @NotNull Path tempFile, @NotNull Path backupFile) {
net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile);
}
@Override
protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
tag.remove(key);
}
}
@@ -16,7 +16,7 @@ import java.util.Optional;
import java.util.UUID;
import java.util.logging.Logger;
public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager {
public class PlayerManager extends com.lishid.openinv.internal.paper1_21_8.player.PlayerManager {
public PlayerManager(@NotNull Logger logger) {
super(logger);
+26
View File
@@ -0,0 +1,26 @@
plugins {
`openinv-base`
alias(libs.plugins.paperweight)
}
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
val paper = candidates.firstOrNull {
it.id.let { id ->
id is ModuleComponentIdentifier && id.module == "paper-api"
}
}
if (paper != null) {
select(paper)
}
because("module is written for Paper servers")
}
}
dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
paperweight.paperDevBundle("1.21.8-R0.1-SNAPSHOT")
}
@@ -0,0 +1,31 @@
package com.lishid.openinv.internal.paper1_21_8;
import com.lishid.openinv.internal.ISpecialPlayerInventory;
import com.lishid.openinv.internal.paper1_21_8.container.OpenInventory;
import com.lishid.openinv.internal.paper1_21_8.player.PlayerManager;
import com.lishid.openinv.util.lang.LanguageManager;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Logger;
public class InternalAccessor extends com.lishid.openinv.internal.common.InternalAccessor {
private final @NotNull PlayerManager manager;
public InternalAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
super(logger, lang);
manager = new PlayerManager(logger);
}
@Override
public @NotNull PlayerManager getPlayerManager() {
return manager;
}
@Override
public @NotNull ISpecialPlayerInventory createPlayerInventory(@NotNull Player player) {
return new OpenInventory(player);
}
}
@@ -0,0 +1,49 @@
package com.lishid.openinv.internal.paper1_21_8.container;
import com.lishid.openinv.internal.common.container.BaseOpenInventory;
import com.lishid.openinv.internal.common.container.menu.OpenChestMenu;
import net.minecraft.ChatFormatting;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class OpenInventory extends BaseOpenInventory {
public OpenInventory(@NotNull Player bukkitPlayer) {
super(bukkitPlayer);
}
@Override
public @NotNull Component getTitle(@Nullable ServerPlayer viewer, @Nullable OpenChestMenu<?> menu) {
MutableComponent component = Component.empty();
// Prefix for use with custom bitmap image fonts.
if (owner.equals(viewer)) {
component.append(
Component.translatableWithFallback("openinv.container.inventory.self", "")
.withStyle(style -> style
.withFont(ResourceLocation.parse("openinv:font/inventory"))
.withColor(ChatFormatting.WHITE)));
} else {
component.append(
Component.translatableWithFallback("openinv.container.inventory.other", "")
.withStyle(style -> style
.withFont(ResourceLocation.parse("openinv:font/inventory"))
.withColor(ChatFormatting.WHITE)));
}
if (menu != null && menu.isViewOnly()) {
component.append(Component.translatableWithFallback("openinv.container.inventory.viewonly", "[RO] "));
} else {
component.append(Component.translatableWithFallback("openinv.container.inventory.editable", ""));
}
// Normal title: "Inventory - OwnerName"
component.append(Component.translatableWithFallback("openinv.container.inventory.prefix", "", owner.getName()))
.append(Component.translatable("container.inventory"))
.append(Component.translatableWithFallback("openinv.container.inventory.suffix", " - %s", owner.getName()));
return component;
}
}
@@ -0,0 +1,58 @@
package com.lishid.openinv.internal.paper1_21_8.player;
import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
import com.mojang.logging.LogUtils;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.level.storage.PlayerDataStorage;
import net.minecraft.world.level.storage.TagValueOutput;
import net.minecraft.world.level.storage.ValueOutput;
import org.bukkit.craftbukkit.CraftServer;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import java.nio.file.Path;
public class OpenPlayer extends BaseOpenPlayer {
protected OpenPlayer(
CraftServer server,
ServerPlayer entity,
PlayerManager manager
) {
super(server, entity, manager);
}
@Override
protected void trySave(ServerPlayer player) {
Logger logger = LogUtils.getLogger();
// See net.minecraft.world.level.storage.PlayerDataStorage#save(EntityHuman)
try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) {
PlayerDataStorage worldNbtStorage = server.getServer().getPlayerList().playerIo;
CompoundTag oldData = isOnline()
? null
: worldNbtStorage.load(player.getName().getString(), player.getStringUUID(), scopedCollector).orElse(null);
CompoundTag playerData = getWritableTag(oldData);
ValueOutput valueOutput = TagValueOutput.createWrappingWithContext(scopedCollector, player.registryAccess(), playerData);
player.saveWithoutId(valueOutput);
saveSafe(player, oldData, playerData, worldNbtStorage);
} catch (Exception e) {
LogUtils.getLogger().warn("Failed to save player data for {}: {}", player.getScoreboardName(), e);
}
}
@Override
protected void safeReplaceFile(@NotNull Path dataFile, @NotNull Path tempFile, @NotNull Path backupFile) {
net.minecraft.Util.safeReplaceFile(dataFile, tempFile, backupFile);
}
@Override
protected void remove(@NotNull CompoundTag tag, @NotNull String key) {
tag.remove(key);
}
}
@@ -0,0 +1,100 @@
package com.lishid.openinv.internal.paper1_21_8.player;
import com.lishid.openinv.internal.common.player.BaseOpenPlayer;
import com.lishid.openinv.util.JulLoggerAdapter;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.ValueInput;
import org.bukkit.Bukkit;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.logging.Logger;
public class PlayerManager extends com.lishid.openinv.internal.common.player.PlayerManager {
public PlayerManager(@NotNull Logger logger) {
super(logger);
}
@Override
protected boolean loadData(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
// See CraftPlayer#loadData
try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) {
ValueInput loadedData = server.getPlayerList().playerIo.load(player, scopedCollector).orElse(null);
if (loadedData == null) {
// Exceptions with loading are logged.
return false;
}
// Read basic data into the player.
player.load(loadedData);
// Game type settings are loaded separately.
player.loadGameTypes(loadedData);
// World is not loaded by ServerPlayer#load(CompoundTag) on Paper.
parseWorld(server, player, loadedData);
}
return true;
}
@Override
protected void spawnInDefaultWorld(@NotNull MinecraftServer server, @NotNull ServerPlayer player) {
ServerLevel level = server.getLevel(Level.OVERWORLD);
if (level != null) {
// Adjust player to default spawn (in keeping with Paper handling) when world not found.
player.snapTo(player.adjustSpawnLocation(level, level.getSharedSpawnPos()).getBottomCenter(), level.getSharedSpawnAngle(), 0.0F);
player.spawnIn(level);
} else {
logger.warning("Tried to load player with invalid world when no fallback was available!");
}
}
@Override
protected void injectPlayer(@NotNull MinecraftServer server, @NotNull ServerPlayer player) throws IllegalAccessException {
if (bukkitEntity == null) {
return;
}
bukkitEntity.setAccessible(true);
bukkitEntity.set(player, new OpenPlayer(server.server, player, this));
}
@Override
public @NotNull Player inject(@NotNull Player player) {
try {
ServerPlayer nmsPlayer = getHandle(player);
if (nmsPlayer.getBukkitEntity() instanceof BaseOpenPlayer openPlayer) {
return openPlayer;
}
MinecraftServer server = nmsPlayer.getServer();
if (server == null) {
if (!(Bukkit.getServer() instanceof CraftServer craftServer)) {
logger.warning(() ->
"Unable to inject ServerPlayer, certain player data may be lost when saving! Server is not a CraftServer: "
+ Bukkit.getServer().getClass().getName());
return player;
}
server = craftServer.getServer();
}
injectPlayer(server, nmsPlayer);
return nmsPlayer.getBukkitEntity();
} catch (IllegalAccessException e) {
logger.log(
java.util.logging.Level.WARNING,
e,
() -> "Unable to inject ServerPlayer, certain player data may be lost when saving!"
);
return player;
}
}
}
+2 -2
View File
@@ -10,9 +10,9 @@ plugins {
apply<SpigotSetup>()
apply<SpigotReobf>()
val spigotVer = "1.21.8-R0.1-SNAPSHOT"
val spigotVer = "1.21.11-R0.1-SNAPSHOT"
// Used by common adapter to relocate Craftbukkit classes to a versioned package.
rootProject.extra["craftbukkitPackage"] = "v1_21_R5"
rootProject.extra["craftbukkitPackage"] = "v1_21_R7"
configurations.all {
resolutionStrategy.capabilitiesResolution.withCapability("org.spigotmc:spigot-api") {
@@ -1,11 +1,11 @@
package com.lishid.openinv.internal.reobf.container.bukkit;
import com.google.common.base.Preconditions;
import com.lishid.openinv.internal.reobf.container.OpenInventory;
import com.lishid.openinv.internal.reobf.container.BaseOpenInventory;
import net.minecraft.core.NonNullList;
import net.minecraft.world.entity.player.Inventory;
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftInventory;
import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.ItemStack;
@@ -15,13 +15,13 @@ import org.jetbrains.annotations.Nullable;
public class OpenPlayerInventory extends CraftInventory implements PlayerInventory {
public OpenPlayerInventory(@NotNull OpenInventory inventory) {
public OpenPlayerInventory(@NotNull BaseOpenInventory inventory) {
super(inventory);
}
@Override
public @NotNull OpenInventory getInventory() {
return (OpenInventory) super.getInventory();
public @NotNull BaseOpenInventory getInventory() {
return (BaseOpenInventory) super.getInventory();
}
@Override
@@ -6,8 +6,8 @@ import net.minecraft.world.Container;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.bukkit.craftbukkit.v1_21_R5.CraftEquipmentSlot;
import org.bukkit.craftbukkit.v1_21_R5.inventory.CraftItemStack;
import org.bukkit.craftbukkit.v1_21_R7.CraftEquipmentSlot;
import org.bukkit.craftbukkit.v1_21_R7.inventory.CraftItemStack;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
@@ -2,7 +2,6 @@ package com.lishid.openinv.internal.reobf.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;
@@ -10,13 +9,12 @@ import net.minecraft.nbt.Tag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.util.ProblemReporter;
import net.minecraft.util.Util;
import net.minecraft.world.level.storage.PlayerDataStorage;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.level.storage.TagValueOutput;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import org.bukkit.craftbukkit.v1_21_R5.CraftServer;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R7.CraftServer;
import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -108,17 +106,9 @@ public class OpenPlayer extends CraftPlayer {
try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), logger)) {
PlayerDataStorage worldNBTStorage = server.getServer().getPlayerList().playerIo;
ValueInput oldDataInput = isOnline()
CompoundTag oldData = isOnline()
? null
: worldNBTStorage.load(player.getName().getString(), player.getStringUUID(), scopedCollector, player.registryAccess()).orElse(null);
// Grab CompoundTag out of TagValueInput - we can't iterate over the fields via TagValueInput.
CompoundTag oldData = null;
if (oldDataInput != null) {
Field tagValueInputInput = TagValueInput.class.getDeclaredField("input");
tagValueInputInput.setAccessible(true);
oldData = (CompoundTag) tagValueInputInput.get(oldDataInput);
}
: worldNBTStorage.load(player.nameAndId()).orElse(null);
CompoundTag playerData = getWritableTag(oldData);
ValueOutput valueOutput = TagValueOutput.createWithContext(scopedCollector, player.registryAccess());
@@ -3,8 +3,10 @@ package com.lishid.openinv.internal.reobf.player;
import com.lishid.openinv.internal.ISpecialInventory;
import com.lishid.openinv.internal.reobf.container.OpenEnderChest;
import com.lishid.openinv.internal.reobf.container.OpenInventory;
import com.lishid.openinv.internal.reobf.container.menu.OpenChestMenu;
import com.lishid.openinv.util.JulLoggerAdapter;
import com.mojang.authlib.GameProfile;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket;
import net.minecraft.server.MinecraftServer;
@@ -17,13 +19,14 @@ import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.ChatVisiblity;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.storage.TagValueInput;
import net.minecraft.world.level.storage.ValueInput;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.Server;
import org.bukkit.craftbukkit.v1_21_R5.CraftServer;
import org.bukkit.craftbukkit.v1_21_R5.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R5.event.CraftEventFactory;
import org.bukkit.craftbukkit.v1_21_R7.CraftServer;
import org.bukkit.craftbukkit.v1_21_R7.entity.CraftPlayer;
import org.bukkit.craftbukkit.v1_21_R7.event.CraftEventFactory;
import org.bukkit.entity.Player;
import org.bukkit.inventory.InventoryView;
import org.jetbrains.annotations.NotNull;
@@ -138,17 +141,17 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
// See CraftPlayer#loadData
try (ProblemReporter.ScopedCollector scopedCollector = new ProblemReporter.ScopedCollector(player.problemPath(), new JulLoggerAdapter(logger))) {
ValueInput loadedData = player.server.getPlayerList().playerIo.load(player, scopedCollector).orElse(null);
CompoundTag loadedData = player.server.getPlayerList().playerIo.load(player.nameAndId()).orElse(null);
if (loadedData == null) {
// Exceptions with loading are logged.
return false;
}
ValueInput valueInput = TagValueInput.create(scopedCollector, player.registryAccess(), loadedData);
// Read basic data into the player.
player.load(loadedData);
// Game type settings are loaded separately.
player.loadGameTypes(loadedData);
player.load(valueInput);
}
return true;
@@ -195,14 +198,14 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
}
// See net.minecraft.server.level.ServerPlayer#openMenu(MenuProvider)
AbstractContainerMenu menu;
OpenChestMenu<?> menu;
Component title;
if (inventory instanceof OpenInventory playerInv) {
menu = playerInv.createMenu(player, player.nextContainerCounter(), viewOnly);
title = playerInv.getTitle(player);
title = playerInv.getTitle(player, menu);
} else if (inventory instanceof OpenEnderChest enderChest) {
menu = enderChest.createMenu(player, player.nextContainerCounter(), viewOnly);
title = enderChest.getTitle();
title = enderChest.getTitle(menu);
} else {
return null;
}
@@ -217,18 +220,18 @@ public class PlayerManager implements com.lishid.openinv.internal.PlayerManager
// resulting in a title change but no other state modifications (like cursor position).
menu.setTitle(title);
menu = CraftEventFactory.callInventoryOpenEvent(player, menu, false);
AbstractContainerMenu opened = CraftEventFactory.callInventoryOpenEvent(player, menu, false);
// Menu is null if event is cancelled.
if (menu == null) {
if (opened == null) {
return null;
}
player.containerMenu = menu;
player.connection.send(new ClientboundOpenScreenPacket(menu.containerId, menu.getType(), menu.getTitle()));
player.initMenu(menu);
player.containerMenu = opened;
player.connection.send(new ClientboundOpenScreenPacket(opened.containerId, opened.getType(), opened.getTitle()));
player.initMenu(opened);
return menu.getBukkitView();
return opened.getBukkitView();
}
}
+7 -1
View File
@@ -13,6 +13,8 @@ dependencies {
implementation(project(":openinvapi"))
implementation(project(":openinvcommon"))
implementation(project(":openinvadaptercommon"))
implementation(project(":openinvadapterpaper1_21_10"))
implementation(project(":openinvadapterpaper1_21_8"))
implementation(project(":openinvadapterpaper1_21_5"))
implementation(project(":openinvadapterpaper1_21_4"))
implementation(project(":openinvadapterpaper1_21_3"))
@@ -20,10 +22,14 @@ dependencies {
implementation(project(":openinvadapterspigot", configuration = SpigotReobf.ARTIFACT_CONFIG))
implementation(libs.planarwrappers)
implementation(libs.folia.scheduler.wrapper)
compileOnly(libs.sqlite.jdbc)
}
tasks.processResources {
expand("version" to version)
expand(
"version" to version,
"sqlite" to libs.sqlite.jdbc.get().version
)
}
tasks.jar {
@@ -56,6 +56,7 @@ import java.nio.file.Path;
import java.util.Locale;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.logging.Level;
/**
* The main class for OpenInv.
@@ -69,6 +70,10 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
private PlayerLoader playerLoader;
private boolean isSpigot = false;
public PlayerLoader getPlayerLoader() {
return playerLoader;
}
@Override
public void reloadConfig() {
super.reloadConfig();
@@ -96,6 +101,11 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
@Override
public void onDisable() {
inventoryManager.evictAll();
try {
playerLoader.getProfileStore().shutdown();
} catch (Exception e) {
getLogger().log(Level.WARNING, "Failed to shut down profile store correctly", e);
}
}
@Override
@@ -244,6 +254,7 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
}
@Override
@Deprecated(forRemoval = true)
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory) {
Permissions edit = null;
HumanEntity target = inventory.getPlayer();
@@ -256,10 +267,12 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
boolean viewOnly = edit != null && !edit.hasPermission(player);
if (ownContainer || (viewOnly && config.getAccessEqualMode() != AccessEqualMode.DENY)) {
if (ownContainer) {
return this.accessor.openInventory(player, inventory, viewOnly);
}
AccessEqualMode accessMode = AccessEqualMode.getByPerm(player, config);
for (int level = 4; level > 0; --level) {
String permission = "openinv.access.level." + level;
// If the target doesn't have this access level...
@@ -273,17 +286,22 @@ public class OpenInv extends FoliaWrappedJavaPlugin implements IOpenInv {
}
// If the viewer doesn't have an equal access level or equal access is a denial, deny.
if (!player.hasPermission(permission) || config.getAccessEqualMode() == AccessEqualMode.DENY) {
if (!player.hasPermission(permission) || accessMode == AccessEqualMode.DENY) {
return null;
}
// Since this is a tie, setting decides view state.
if (config.getAccessEqualMode() == AccessEqualMode.VIEW) {
if (accessMode == AccessEqualMode.VIEW) {
viewOnly = true;
}
break;
}
return openInventory(player, inventory, viewOnly);
}
@Override
public @Nullable InventoryView openInventory(@NotNull Player player, @NotNull ISpecialInventory inventory, boolean viewOnly) {
return this.accessor.openInventory(player, inventory, viewOnly);
}
@@ -87,10 +87,11 @@ public class ClearInvCommand extends PlayerLookupCommand {
@Override
protected void handle(
@NotNull CommandSender sender,
@NotNull Player onlineTarget,
@NotNull PlayerAccess playerAccess,
boolean accessInv,
@NotNull String @NotNull [] args
) {
Player onlineTarget = playerAccess.player();
// Create the inventory
final ISpecialInventory inv;
try {
@@ -151,11 +151,12 @@ public class OpenInvCommand extends PlayerLookupCommand {
@Override
protected void handle(
@NotNull CommandSender sender,
@NotNull Player target,
@NotNull PlayerAccess playerAccess,
boolean accessInv,
@NotNull String @NotNull [] args
) {
Player player = (Player) sender;
Player target = playerAccess.player();
if (!config.doesNoArgsOpenSelf()) {
// Record the target
(accessInv ? this.openInvHistory : this.openEnderHistory).put(player, target.getUniqueId().toString());
@@ -172,7 +173,7 @@ public class OpenInvCommand extends PlayerLookupCommand {
}
// Open the inventory
plugin.openInventory(player, inv);
plugin.openInventory(player, inv, playerAccess.viewOnly());
}
}
@@ -79,7 +79,7 @@ public abstract class PlayerLookupCommand implements TabExecutor {
}
// Perform access checks and load target if necessary.
Player onlineTarget = access(sender, target, accessInv);
PlayerAccess onlineTarget = access(sender, target, accessInv);
if (onlineTarget != null) {
handle(sender, onlineTarget, accessInv, args);
@@ -135,7 +135,11 @@ public abstract class PlayerLookupCommand implements TabExecutor {
* @param invPerms {@code true} to use inventory permissions, {@code false} for ender chest
* @return the {@link Player} loaded or {@code null} if target is not accessible
*/
protected @Nullable Player access(@NotNull CommandSender sender, @NotNull OfflinePlayer target, boolean invPerms) {
protected @Nullable PlayerAccess access(
@NotNull CommandSender sender,
@NotNull OfflinePlayer target,
boolean invPerms
) {
// Attempt to load online player dependent on permissions and configuration.
Player onlineTarget = accessAsPlayer(sender, target);
@@ -144,11 +148,11 @@ public abstract class PlayerLookupCommand implements TabExecutor {
}
// Permissions checks.
if (deniedCommand(sender, onlineTarget, invPerms) || deniedAccess(sender, onlineTarget)) {
if (deniedCommand(sender, onlineTarget, invPerms)) {
return null;
}
return onlineTarget;
return accessGeneralized(sender, onlineTarget, invPerms);
}
/**
@@ -203,30 +207,33 @@ public abstract class PlayerLookupCommand implements TabExecutor {
);
/**
* Check for a lack of generalized permissions for accessing the target.
* Check for generalized permissions for accessing the target.
* By default, this is access levels and cross-world restrictions.
*
* @param sender the {@link CommandSender} attempting access
* @param onlineTarget the {@link Player} being targeted by the command
* @return {@code true} if the sender does not have access to the target
* @param invPerms {@code true} to use inventory permissions, {@code false} for ender chest
* @return a {@link PlayerAccess} containing the accessed player and view mode, or {@code null} if denied
*/
protected boolean deniedAccess(@NotNull CommandSender sender, @NotNull Player onlineTarget) {
if (sender.equals(onlineTarget)) {
return false;
protected @Nullable PlayerAccess accessGeneralized(
@NotNull CommandSender sender,
@NotNull Player onlineTarget,
boolean invPerms
) {
boolean ownContainer = sender.equals(onlineTarget);
Permissions edit;
if (invPerms) {
edit = ownContainer ? Permissions.INVENTORY_EDIT_SELF : Permissions.INVENTORY_EDIT_OTHER;
} else {
edit = ownContainer ? Permissions.ENDERCHEST_EDIT_SELF : Permissions.ENDERCHEST_EDIT_OTHER;
}
// Protected check
for (int level = 4; level > 0; --level) {
String permission = "openinv.access.level." + level;
if (onlineTarget.hasPermission(permission)
&& (!sender.hasPermission(permission) || config.getAccessEqualMode() == AccessEqualMode.DENY)) {
lang.sendMessage(
sender,
"messages.error.permissionExempt",
new Replacement("%target%", onlineTarget.getDisplayName())
);
return true;
}
boolean viewOnly = !edit.hasPermission(sender);
if (ownContainer) {
// Skip other access checks for self.
return new PlayerAccess(onlineTarget, viewOnly);
}
// Crossworld check
@@ -238,10 +245,41 @@ public abstract class PlayerLookupCommand implements TabExecutor {
"messages.error.permissionCrossWorld",
new Replacement("%target%", onlineTarget.getDisplayName())
);
return true;
return null;
}
return false;
AccessEqualMode accessMode = AccessEqualMode.getByPerm(sender, config);
for (int level = 4; level > 0; --level) {
String permission = "openinv.access.level." + level;
// If the target doesn't have this access level...
if (!onlineTarget.hasPermission(permission)) {
// If the viewer does have the access level, all good.
if (sender.hasPermission(permission)) {
break;
}
// Otherwise check next access level.
continue;
}
// If the viewer doesn't have an equal access level or equal access is a denial, deny.
if (!sender.hasPermission(permission) || accessMode == AccessEqualMode.DENY) {
lang.sendMessage(
sender,
"messages.error.permissionExempt",
new Replacement("%target%", onlineTarget.getDisplayName())
);
return null;
}
// Since this is a tie, setting decides view state.
if (accessMode == AccessEqualMode.VIEW) {
viewOnly = true;
}
break;
}
return new PlayerAccess(onlineTarget, viewOnly);
}
/**
@@ -254,7 +292,7 @@ public abstract class PlayerLookupCommand implements TabExecutor {
*/
protected abstract void handle(
@NotNull CommandSender sender,
@NotNull Player target,
@NotNull PlayerAccess target,
boolean accessInv,
@NotNull String @NotNull [] args
);
@@ -273,4 +311,6 @@ public abstract class PlayerLookupCommand implements TabExecutor {
return TabCompleter.completeOnlinePlayer(sender, args[0]);
}
protected record PlayerAccess(Player player, boolean viewOnly) {}
}
@@ -39,8 +39,6 @@ import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
/**
* A listener managing AnyContainer, SilentContainer, and more.
*/
@@ -71,15 +69,14 @@ public class ContainerListener implements Listener {
}
Player player = event.getPlayer();
UUID playerId = player.getUniqueId();
boolean any = Permissions.CONTAINER_ANY.hasPermission(player) && PlayerToggles.any().is(playerId);
boolean any = PlayerToggles.any().is(player);
boolean needsAny = accessor.getAnySilentContainer().isAnyContainerNeeded(event.getClickedBlock());
if (!any && needsAny) {
return;
}
boolean silent = Permissions.CONTAINER_SILENT.hasPermission(player) && PlayerToggles.silent().is(playerId);
boolean silent = PlayerToggles.silent().is(player);
// If anycontainer or silentcontainer is active
if (any || silent) {
@@ -104,7 +101,7 @@ public class ContainerListener implements Listener {
}
InventoryHolder holder = event.getInventory().getHolder();
if (PlayerToggles.silent().is(player.getUniqueId())
if (PlayerToggles.silent().is(player)
&& holder != null
&& this.accessor.getAnySilentContainer().isAnySilentContainer(holder)) {
this.accessor.getAnySilentContainer().deactivateContainer(player);
@@ -1,5 +1,7 @@
package com.lishid.openinv.util;
import com.lishid.openinv.util.config.Config;
import org.bukkit.permissions.Permissible;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -20,4 +22,17 @@ public enum AccessEqualMode {
};
}
public static @NotNull AccessEqualMode getByPerm(@NotNull Permissible permissible, @NotNull Config config) {
if (Permissions.ACCESS_EQUAL_EDIT.hasPermission(permissible)) {
return AccessEqualMode.ALLOW;
}
if (Permissions.ACCESS_EQUAL_VIEW.hasPermission(permissible)) {
return AccessEqualMode.VIEW;
}
if (Permissions.ACCESS_EQUAL_DENY.hasPermission(permissible)) {
return AccessEqualMode.DENY;
}
return config.getAccessEqualMode();
}
}
@@ -64,7 +64,7 @@ public class InternalAccessor {
}
private @Nullable Accessor getAccessor(@NotNull Logger logger, @NotNull LanguageManager lang) {
Version maxSupported = Version.of(1, 21, 8);
Version maxSupported = Version.of(1, 21, 11);
Version minSupported = Version.of(1, 21, 1);
// Ensure version is in supported range.
@@ -74,7 +74,7 @@ public class InternalAccessor {
// Load Spigot accessor.
if (!PAPER) {
if (BukkitVersions.MINECRAFT.equals(maxSupported) || BukkitVersions.MINECRAFT.equals(Version.of(1, 21, 7))) {
if (BukkitVersions.MINECRAFT.equals(maxSupported)) {
// Current Spigot, remapped internals are available.
return new com.lishid.openinv.internal.reobf.InternalAccessor(logger, lang);
} else {
@@ -84,10 +84,17 @@ public class InternalAccessor {
}
// Paper or a Paper fork, can use Mojang-mapped internals.
if (BukkitVersions.MINECRAFT.lessThanOrEqual(maxSupported)
&& BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 6))) { // 1.21.6, 1.21.7, 1.21.8
if (BukkitVersions.MINECRAFT.equals(maxSupported)) { // 1.21.11
return new com.lishid.openinv.internal.common.InternalAccessor(logger, lang);
}
if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 10))
&& BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 9))) { // 1.21.9, 1.21.10
return new com.lishid.openinv.internal.paper1_21_10.InternalAccessor(logger, lang);
}
if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 8))
&& BukkitVersions.MINECRAFT.greaterThanOrEqual(Version.of(1, 21, 6))) { // 1.21.6, 1.21.7, 1.21.8
return new com.lishid.openinv.internal.paper1_21_8.InternalAccessor(logger, lang);
}
if (BukkitVersions.MINECRAFT.equals(Version.of(1, 21, 5))) { // 1.21.5
return new com.lishid.openinv.internal.paper1_21_5.InternalAccessor(logger, lang);
}
@@ -176,7 +183,7 @@ public class InternalAccessor {
if (!PAPER) {
return getSpigotReleaseLink();
}
// Paper 1.21.1-1.21.7
// Paper 1.21.1+
return "https://github.com/Jikoo/OpenInv/releases";
}
@@ -196,6 +203,15 @@ public class InternalAccessor {
if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 6))) {
return "Unsupported; upgrade to 1.21.7: https://github.com/Jikoo/OpenInv/releases";
}
if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 8))) {
return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.13";
}
if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 9))) {
return "Unsupported; upgrade to 1.21.10: https://github.com/Jikoo/OpenInv/releases/tag/5.1.15";
}
if (BukkitVersions.MINECRAFT.lessThanOrEqual(Version.of(1, 21, 10))) {
return "https://github.com/Jikoo/OpenInv/releases/tag/5.1.15";
}
return "https://github.com/Jikoo/OpenInv/releases";
}
@@ -5,13 +5,16 @@ import com.google.common.cache.CacheBuilder;
import com.google.errorprone.annotations.Keep;
import com.lishid.openinv.OpenInv;
import com.lishid.openinv.util.config.Config;
import com.lishid.openinv.util.profile.OfflinePlayerProfileStore;
import com.lishid.openinv.util.profile.Profile;
import com.lishid.openinv.util.profile.ProfileStore;
import com.lishid.openinv.util.profile.sqlite.SqliteProfileStore;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.profile.PlayerProfile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -33,7 +36,8 @@ public class PlayerLoader implements Listener {
private final @NotNull InventoryManager inventoryManager;
private final @NotNull InternalAccessor internalAccessor;
private final @NotNull Logger logger;
private final @NotNull Cache<String, PlayerProfile> lookupCache;
private final @NotNull Cache<String, Profile> lookupCache;
private @NotNull ProfileStore profileStore;
public PlayerLoader(
@NotNull OpenInv plugin,
@@ -46,10 +50,30 @@ public class PlayerLoader implements Listener {
this.config = config;
this.inventoryManager = inventoryManager;
this.internalAccessor = internalAccessor;
try {
SqliteProfileStore sqliteStore = new SqliteProfileStore(plugin);
sqliteStore.setup();
sqliteStore.tryImport();
this.profileStore = sqliteStore;
} catch (Exception e) {
this.profileStore = new OfflinePlayerProfileStore(logger);
}
this.logger = logger;
this.lookupCache = CacheBuilder.newBuilder().maximumSize(20).build();
}
public @NotNull ProfileStore getProfileStore() {
return profileStore;
}
public void setProfileStore(@NotNull ProfileStore profileStore) {
plugin.getLogger().log(
Level.INFO,
() -> "Setting profile store implementation to " + profileStore.getClass().getName()
);
this.profileStore = profileStore;
}
/**
* 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}.
@@ -59,22 +83,20 @@ public class PlayerLoader implements Listener {
* @throws IllegalStateException if the server version is unsupported
*/
public @Nullable Player load(@NotNull OfflinePlayer offline) {
UUID key = offline.getUniqueId();
Player player = offline.getPlayer();
if (player != null) {
return player;
}
player = inventoryManager.getLoadedPlayer(key);
if (player != null) {
return player;
}
if (config.isOfflineDisabled() || !internalAccessor.isSupported()) {
return null;
}
player = inventoryManager.getLoadedPlayer(offline.getUniqueId());
if (player != null) {
return player;
}
if (Bukkit.isPrimaryThread()) {
return internalAccessor.getPlayerDataManager().loadPlayer(offline);
}
@@ -93,13 +115,6 @@ public class PlayerLoader implements Listener {
}
public @Nullable OfflinePlayer matchExact(@NotNull String name) {
// Warn if called on the main thread - if we resort to searching offline players, this may take several seconds.
if (Bukkit.getServer().isPrimaryThread()) {
logger.warning("Call to PlayerSearchCache#matchPlayer made on the main thread!");
logger.warning("This can cause the server to hang, potentially severely.");
logger.log(Level.WARNING, "Current stack trace", new Throwable("Current stack trace"));
}
OfflinePlayer player;
try {
@@ -123,9 +138,9 @@ public class PlayerLoader implements Listener {
}
// Cached offline match.
PlayerProfile cachedResult = lookupCache.getIfPresent(name);
if (cachedResult != null && cachedResult.getUniqueId() != null) {
player = Bukkit.getOfflinePlayer(cachedResult.getUniqueId());
Profile cachedResult = lookupCache.getIfPresent(name);
if (cachedResult != null) {
player = Bukkit.getOfflinePlayer(cachedResult.id());
// Ensure player is an existing player.
if (player.hasPlayedBefore() || player.isOnline()) {
return player;
@@ -135,10 +150,15 @@ public class PlayerLoader implements Listener {
}
// Exact offline match second - ensure offline access works when matchable users are online.
player = Bukkit.getServer().getOfflinePlayer(name);
Profile profile = profileStore.getProfileExact(name);
if (profile == null) {
return null;
}
player = Bukkit.getOfflinePlayer(profile.id());
if (player.hasPlayedBefore()) {
lookupCache.put(name, player.getPlayerProfile());
lookupCache.put(name, profile);
return player;
}
@@ -160,40 +180,35 @@ public class PlayerLoader implements Listener {
}
// 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;
}
Profile profile = getProfileStore().getProfileInexact(name);
float currentMatch = StringMetric.compareJaroWinkler(name, offline.getName());
if (currentMatch == 1.0F) {
return offline;
}
if (currentMatch > bestMatch) {
bestMatch = currentMatch;
player = offline;
}
if (profile == null) {
// No match found.
return null;
}
if (player != null) {
// If a match was found, store it.
lookupCache.put(name, player.getPlayerProfile());
// Get associated player and store match.
player = Bukkit.getOfflinePlayer(profile.id());
if (player.hasPlayedBefore()) {
lookupCache.put(name, profile);
return player;
}
// No players have ever joined the server.
return null;
}
@Keep
@EventHandler
private void onPlayerJoin(@NotNull PlayerJoinEvent event) {
plugin.getScheduler().runTaskLaterAsynchronously(() -> updateMatches(event), 7L);
}
private void updateMatches(@NotNull PlayerJoinEvent event) {
// Update profile store.
profileStore.addProfile(new Profile(event.getPlayer()));
// If player is not new, any cached values are valid.
if (event.getPlayer().hasPlayedBefore()) {
if (event.getPlayer().hasPlayedBefore() || lookupCache.size() == 0) {
return;
}
@@ -201,36 +216,19 @@ public class PlayerLoader implements Listener {
String name = event.getPlayer().getName();
lookupCache.invalidate(name);
// If the cache is empty, nothing to do. Don't hit scheduler.
if (lookupCache.size() == 0) {
return;
Iterator<Map.Entry<String, Profile>> iterator = lookupCache.asMap().entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Profile> entry = iterator.next();
String oldMatch = entry.getValue().name();
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();
}
}
plugin.getScheduler().runTaskLaterAsynchronously(
() -> {
Iterator<Map.Entry<String, PlayerProfile>> iterator = lookupCache.asMap().entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, PlayerProfile> entry = iterator.next();
String oldMatch = entry.getValue().getName();
// Shouldn't be possible - all profiles should be complete.
if (oldMatch == null) {
iterator.remove();
continue;
}
String lookup = entry.getKey();
float oldMatchScore = StringMetric.compareJaroWinkler(lookup, oldMatch);
float newMatchScore = StringMetric.compareJaroWinkler(lookup, name);
// If new match exceeds old match, delete old match.
if (newMatchScore > oldMatchScore) {
iterator.remove();
}
}
},
7L
);
}
}
@@ -0,0 +1,78 @@
package com.lishid.openinv.util.profile;
import com.github.jikoo.planarwrappers.scheduler.TickTimeUnit;
import me.nahu.scheduler.wrapper.WrappedJavaPlugin;
import me.nahu.scheduler.wrapper.runnable.WrappedRunnable;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
public abstract class BatchProfileStore implements ProfileStore {
private final Set<Profile> pending = Collections.synchronizedSet(new HashSet<>());
private final AtomicReference<WrappedRunnable> insertTask = new AtomicReference<>();
protected final @NotNull WrappedJavaPlugin plugin;
protected BatchProfileStore(@NotNull WrappedJavaPlugin plugin) {
this.plugin = plugin;
}
@Override
public void addProfile(@NotNull Profile profile) {
pending.add(profile);
buildBatch();
}
private void buildBatch() {
if (insertTask.compareAndSet(null, new WrappedRunnable() {
@Override
public void run() {
pushBatch();
}
})) {
try {
// Wait 5 seconds to accumulate other player data to reduce scheduler load on larger servers.
insertTask.get().runTaskLaterAsynchronously(plugin, TickTimeUnit.toTicks(5, TimeUnit.SECONDS));
} catch (IllegalStateException e) {
// If scheduling task fails, server is most likely shutting down.
insertTask.set(null);
}
}
}
private void pushBatch() {
Set<Profile> batch = new HashSet<>(pending);
// This is a bit roundabout but removes the risk of data loss.
pending.removeAll(batch);
// Push current batch.
pushBatch(batch);
WrappedRunnable running = insertTask.getAndSet(null);
if (running != null) {
running.cancel();
}
// If more profiles have been added, build another batch.
if (!pending.isEmpty()) {
buildBatch();
}
}
@Override
public void shutdown() {
WrappedRunnable wrappedRunnable = insertTask.get();
if (wrappedRunnable != null) {
wrappedRunnable.cancel();
}
pushBatch();
insertTask.set(null);
}
protected abstract void pushBatch(@NotNull Set<Profile> batch);
}
@@ -0,0 +1,48 @@
package com.lishid.openinv.util.profile;
import me.nahu.scheduler.wrapper.runnable.WrappedRunnable;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.Set;
public abstract class OfflinePlayerImporter extends WrappedRunnable {
private final @NotNull BatchProfileStore profileStore;
private final int batchSize;
public OfflinePlayerImporter(@NotNull BatchProfileStore profileStore, int batchSize) {
this.profileStore = profileStore;
this.batchSize = batchSize;
}
@Override
public void run() {
Set<Profile> batch = new HashSet<>();
for (OfflinePlayer offline : Bukkit.getOfflinePlayers()) {
String name = offline.getName();
if (name == null) {
continue;
}
batch.add(new Profile(name, offline.getUniqueId()));
if (batch.size() >= batchSize) {
profileStore.pushBatch(batch);
batch.clear();
}
}
if (!batch.isEmpty()) {
profileStore.pushBatch(batch);
}
onComplete();
}
public abstract void onComplete();
}
@@ -0,0 +1,86 @@
package com.lishid.openinv.util.profile;
import com.lishid.openinv.util.StringMetric;
import org.bukkit.Bukkit;
import org.bukkit.OfflinePlayer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Logger;
public class OfflinePlayerProfileStore implements ProfileStore {
private final @NotNull Logger logger;
public OfflinePlayerProfileStore(@NotNull Logger logger) {
this.logger = logger;
}
@Override
public void addProfile(@NotNull Profile profile) {
// No-op. Server handles profile creation and storage.
}
@Override
public void setup() {
// No-op.
}
@Override
public void shutdown() {
// No-op. Nothing to push.
}
@Override
public void tryImport() {
// No-op.
}
@Override
public @Nullable Profile getProfileExact(@NotNull String name) {
ProfileStore.warnMainThread(logger);
@SuppressWarnings("deprecation")
OfflinePlayer offline = Bukkit.getOfflinePlayer(name);
if (!offline.hasPlayedBefore()) {
return null;
}
String realName = offline.getName();
if (realName == null) {
realName = name;
}
return new Profile(realName, offline.getUniqueId());
}
@Override
public @Nullable Profile getProfileInexact(@NotNull String search) {
ProfileStore.warnMainThread(logger);
float bestMatch = 0.0F;
Profile bestProfile = null;
for (OfflinePlayer player : Bukkit.getOfflinePlayers()) {
String name = player.getName();
if (name == null) {
// Discount UUID-only profiles; Direct UUID lookup should already be done.
return null;
}
float currentMatch = StringMetric.compareJaroWinkler(name, name);
if (currentMatch > bestMatch) {
bestMatch = currentMatch;
bestProfile = new Profile(name, player.getUniqueId());
}
// A score of 1 is an exact match. Don't bother checking the rest.
if (currentMatch == 1.0F) {
break;
}
}
return bestProfile;
}
}
@@ -0,0 +1,20 @@
package com.lishid.openinv.util.profile;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public record Profile(@NotNull String name, @NotNull UUID id) {
public Profile(@NotNull Player player) {
this(player.getName(), player.getUniqueId());
}
@Override
public int hashCode() {
// As names are the unique key, profiles with the same name should collide.
return name.hashCode();
}
}
@@ -0,0 +1,51 @@
package com.lishid.openinv.util.profile;
import org.bukkit.Bukkit;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.logging.Level;
import java.util.logging.Logger;
public interface ProfileStore {
void addProfile(@NotNull Profile profile);
void setup() throws Exception;
void shutdown() throws Exception;
void tryImport() throws Exception;
@Nullable Profile getProfileExact(@NotNull String name);
@Nullable Profile getProfileInexact(@NotNull String search);
static void warnMainThread(@NotNull Logger logger) {
if (!Bukkit.getServer().isPrimaryThread()) {
return;
}
Throwable throwable = new Throwable("Current stack trace");
StackTraceElement[] stackTrace = throwable.getStackTrace();
if (stackTrace.length < 2) {
// Not possible.
return;
}
StackTraceElement caller = stackTrace[1];
logger.warning(() ->
String.format(
"Call to %s#%s made on the main thread!",
caller.getClassName(),
caller.getMethodName()
)
);
logger.warning("This can cause the server to hang, potentially severely.");
logger.log(Level.WARNING, "Current stack trace", throwable);
}
}
@@ -0,0 +1,21 @@
package com.lishid.openinv.util.profile.sqlite;
import com.lishid.openinv.util.StringMetric;
import org.sqlite.Function;
import java.sql.SQLException;
public class JaroWinklerFunction extends Function {
@Override
protected void xFunc() throws SQLException {
if (args() != 2) {
throw new SQLException("JaroWinkler(str, str) requires 2 arguments but got " + args());
}
String val1 = value_text(0);
String val2 = value_text(1);
result(StringMetric.compareJaroWinkler(val1 == null ? "" : val1, val2 == null ? "" : val2));
}
}
@@ -0,0 +1,257 @@
package com.lishid.openinv.util.profile.sqlite;
import com.github.jikoo.planarwrappers.function.ThrowingFunction;
import com.lishid.openinv.util.profile.BatchProfileStore;
import com.lishid.openinv.util.profile.OfflinePlayerImporter;
import com.lishid.openinv.util.profile.Profile;
import me.nahu.scheduler.wrapper.WrappedJavaPlugin;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.sqlite.Function;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Iterator;
import java.util.Set;
import java.util.UUID;
import java.util.logging.Level;
public class SqliteProfileStore extends BatchProfileStore {
private static final int BATCH_SIZE = 1_000;
private static final int MAX_BATCHES = 20;
private Connection connection;
public SqliteProfileStore(@NotNull WrappedJavaPlugin plugin) {
super(plugin);
}
@Override
public void setup() throws Exception {
// Touch implementation to ensure it is available, then create connection.
Class.forName("org.sqlite.JDBC");
connection = DriverManager.getConnection("jdbc:sqlite:" + plugin.getDataFolder().getAbsolutePath() + "/profiles.db");
try (Statement statement = connection.createStatement()) {
// Create main profile table.
statement.executeUpdate(
"""
CREATE TABLE IF NOT EXISTS profiles(
name TEXT PRIMARY KEY,
uuid_least INTEGER NOT NULL,
uuid_most INTEGER NOT NULL,
last_update TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
) WITHOUT ROWID
"""
);
// Create meta table.
statement.executeUpdate(
"""
CREATE TABLE IF NOT EXISTS openinv_meta(
name TEXT PRIMARY KEY,
value INTEGER NOT NULL
)
"""
);
}
// Add JaroWinkler function to the connection.
Function.create(connection, "JaroWinkler", new JaroWinklerFunction());
}
@Override
public void shutdown() {
super.shutdown();
try {
if (!connection.getAutoCommit()) {
connection.commit();
}
connection.close();
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Exception closing database: " + e.getMessage(), e);
}
}
@Override
public void tryImport() throws SQLException {
try (PreparedStatement statement = connection.prepareStatement(
"SELECT value FROM openinv_meta WHERE name = 'imported'"
)) {
ResultSet resultSet = statement.executeQuery();
if (resultSet.next() && resultSet.getInt("value") == 1) {
return;
}
}
plugin.getLogger().info("Indexing player names.");
new OfflinePlayerImporter(this, BATCH_SIZE) {
@Override
public void onComplete() {
try (PreparedStatement statement = connection.prepareStatement(
"INSERT INTO openinv_meta(name, value) VALUES ('imported', 1)"
)) {
statement.executeUpdate();
plugin.getLogger().info("Indexed player names successfully.");
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Exception marking import complete: " + e.getMessage(), e);
}
}
}.runTaskAsynchronously(plugin);
}
@Override
protected void pushBatch(@NotNull Set<Profile> batch) {
if (batch.isEmpty()) {
return;
}
int batchSize = Math.min(batch.size(), BATCH_SIZE);
int rem = batch.size() % batchSize;
Iterator<Profile> iterator = batch.iterator();
try {
boolean autoCommit = connection.getAutoCommit();
if (autoCommit) {
connection.setAutoCommit(false);
}
pushBatch(iterator, batch.size(), batchSize);
if (rem > 0) {
pushBatch(iterator, rem, rem);
}
connection.commit();
if (autoCommit) {
connection.setAutoCommit(true);
}
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Encountered an exception updating profiles", e);
}
}
private void pushBatch(Iterator<Profile> iterator, int iteratorSize, int batchSize) throws SQLException {
try (PreparedStatement upsert = createBulkUpsertProfile(batchSize)) {
for (int batchIndex = 0; batchIndex < iteratorSize / batchSize; ++batchIndex) {
for (int entryIndex = 0; entryIndex < batchSize; ++entryIndex) {
int startIndex = entryIndex * 3;
Profile profile = iterator.next();
upsert.setString(startIndex + 1, profile.name());
upsert.setLong(startIndex + 2, profile.id().getLeastSignificantBits());
upsert.setLong(startIndex + 3, profile.id().getMostSignificantBits());
}
upsert.addBatch();
// If we're at the maximum number of batches allowed, commit.
if ((batchIndex + 1) % MAX_BATCHES == 0) {
upsert.executeBatch();
connection.commit();
}
}
upsert.executeBatch();
}
}
private @NotNull PreparedStatement createBulkUpsertProfile(int count) throws SQLException {
return connection.prepareStatement(
"""
INSERT INTO profiles(name, uuid_least, uuid_most)
VALUES (?, ?, ?)
""" + ", (?, ?, ?)".repeat(count - 1) +
"""
ON CONFLICT(name) DO UPDATE SET
uuid_least=excluded.uuid_least,
uuid_most=excluded.uuid_most,
last_update=CURRENT_TIMESTAMP
"""
);
}
@Override
public @Nullable Profile getProfileExact(@NotNull String name) {
return getProfile(
name,
text -> {
PreparedStatement statement = connection.prepareStatement(
"SELECT name, uuid_least, uuid_most FROM `profiles` WHERE name = ?"
);
statement.setString(1, text);
return statement;
}
);
}
@Override
public @Nullable Profile getProfileInexact(@NotNull String search) {
return getProfile(
search,
text -> {
PreparedStatement statement = connection.prepareStatement(
"""
SELECT name, uuid_least, uuid_most FROM `profiles`
WHERE name LIKE ?
ORDER BY JaroWinkler(?, name) DESC
LIMIT 1
"""
);
statement.setString(1, getLikePrefix(text));
statement.setString(2, text);
return statement;
}
);
}
private @NotNull String getLikePrefix(@NotNull String search) {
StringBuilder prefix = new StringBuilder();
int searchLen = search.length();
if (searchLen > 0) {
char first = search.charAt(0);
// Add first character of the search to LIKE prefix.
prefix.append(first);
// If the first character appears to be Geyser-like, append another.
if (searchLen > 1 && (first == '.' || first == '*')) {
prefix.append(search.charAt(1));
}
}
// Add wildcard.
prefix.append('%');
return prefix.toString();
}
private @Nullable Profile getProfile(
@NotNull String text,
@NotNull ThrowingFunction<String, PreparedStatement, SQLException> create
) {
try (PreparedStatement statement = create.apply(text)) {
ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) {
// Fetch profile data.
String name = resultSet.getString(1);
// Ignore illegal data.
if (name == null || name.isEmpty()) {
return null;
}
UUID uuid = new UUID(resultSet.getLong(3), resultSet.getLong(2));
return new Profile(name, uuid);
}
} catch (SQLException e) {
plugin.getLogger().log(Level.WARNING, "Encountered an exception retrieving profile", e);
}
return null;
}
}
+16 -2
View File
@@ -7,6 +7,9 @@ description: Open a player's inventory as a chest and interact with it in real t
api-version: "1.13"
folia-supported: true
libraries:
- "org.xerial:sqlite-jdbc:${sqlite}"
permissions:
openinv:
@@ -66,6 +69,9 @@ permissions:
openinv.access.level.2: false
openinv.access.level.3: false
openinv.access.level.4: false
openinv.access.equal.edit: false
openinv.access.equal.view: false
openinv.access.equal.deny: false
# Spectate features
openinv.spectate:
children:
@@ -73,8 +79,16 @@ permissions:
# Container features
openinv.container:
children:
openinv.container.any: true
openinv.container.silent: true
openinv.container.any:
children:
openinv.container.any.use: true
openinv.container.any.use:
default: false
openinv.container.silent:
children:
openinv.container.silent.use: true
openinv.container.silent.use:
default: false
# Search functionality
openinv.search:
children:
@@ -1,13 +1,17 @@
{
"pack": {
"description": "Improve OpenInv's legibility",
"pack_format": 55,
"supported_formats": [ 34, 64 ]
"min_format": 34,
"max_format": [75, 0],
"pack_format": 64,
"supported_formats": [ 34, 75 ]
},
"overlays": {
"entries": [
{
"formats": [ 44, 64 ],
"min_format": 44,
"max_format": [75, 0],
"formats": [ 44, 75 ],
"directory": "openinv_44"
},
{
-58
View File
@@ -1,58 +0,0 @@
#!/bin/bash
#
# Copyright (C) 2011-2021 lishid. All rights reserved.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# Note that this script is designed for use in GitHub Actions, and is not
# particularly robust nor configurable. Run from project parent directory.
if [[ ! $1 ]]; then
echo "No changelog, no Minecraft versions."
exit 0
fi
# Find line declaring Paper versions.
raw=$(grep "**Paper:**" <<< "$1")
# Enable extended glob pattern to match 0 or more whitespace characters.
shopt -s extglob
# Trim Paper versions identifier prefix.
raw=${raw##*([[:space:]])'**'Paper:'**'*([[:space:]])}
# Replace commas and optional spaces with a newline.
raw=${raw//,*([[:space:]])/$'\n'}
# Turn extglob back off.
shopt -u extglob
# Split into an array on newlines.
readarray -td $'\n' versions <<< "${raw}"
for version in "${versions[@]}"; do
# Parse Minecraft minor version by dropping everything from the second period onward.
# CurseForge doesn't usually add patch versions for Bukkit, so we're more likely to
# hit a supported identifier this way.
version="${version%[.-]"${version#*.*[.-]}"}"
# Skip already listed versions
if [[ "$minecraft_versions" =~ "$version"($|,) ]]; then
continue
fi
# Append comma if variable is set, then append version.
# Note that Minecraft versions on CurseForge are declared "Minecraft x.y.z"
minecraft_versions="${minecraft_versions:+${minecraft_versions},}Minecraft ${version}"
done
printf "$minecraft_versions\n"
#echo "CURSEFORGE_MINECRAFT_VERSIONS=$minecraft_versions" >> "$GITHUB_ENV"
+2
View File
@@ -19,6 +19,8 @@ if (!java.lang.Boolean.getBoolean("jitpack")) {
val internals = listOf(
"common",
"paper1_21_10",
"paper1_21_8",
"paper1_21_5",
"paper1_21_4",
"paper1_21_3",