Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
076d999005 | ||
|
|
d421ff2a85 | ||
|
|
494e18b128 | ||
|
|
a786471adb | ||
|
|
2d09397800 |
No files matched your search
@@ -10,14 +10,14 @@ jobs:
|
||||
steps:
|
||||
# Download code from repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Setup Java
|
||||
- name: Setup JDK
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '17'
|
||||
|
||||
# Build plugin
|
||||
- name: Build with Maven
|
||||
|
||||
+12
-19
@@ -6,23 +6,23 @@ jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 8
|
||||
continue-on-error: ${{ matrix.version == '1.20.4' }}
|
||||
continue-on-error: ${{ matrix.version == '1.20.2' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: ['bukkit', 'spigot', 'paper', 'purpur']
|
||||
version: ['1.20.4', '1.19.4', '1.18.2', '1.17.1', '1.16.5']
|
||||
version: ['1.20.2', '1.19.4', '1.18.2', '1.17.1', '1.16.5']
|
||||
steps:
|
||||
# Download code from repository
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Setup Java (for building)
|
||||
- name: Setup JDK (for building)
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '21'
|
||||
java-version: '17'
|
||||
|
||||
# Build plugin
|
||||
- name: Build with Maven
|
||||
@@ -30,21 +30,14 @@ jobs:
|
||||
|
||||
# Setup Java (for testing)
|
||||
- name: Setup JDK (for testing)
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: |
|
||||
${{
|
||||
startsWith(matrix.version, '1.16') && '11' ||
|
||||
startsWith(matrix.version, '1.17') && '16' ||
|
||||
startsWith(matrix.version, '1.18') && '17' ||
|
||||
startsWith(matrix.version, '1.19') && '17' ||
|
||||
'21'
|
||||
}}
|
||||
java-version: ${{ startsWith(matrix.version, '1.16') && '11' || (startsWith(matrix.version, '1.17') && '16' || '17') }}
|
||||
|
||||
# Setup Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
@@ -116,19 +109,19 @@ jobs:
|
||||
echo "Plugin did not enabled verbose mode"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Fq '[YamipaPlugin] [ImageStorage] Found 5 file(s) in images directory' server.log; then
|
||||
if ! grep -Fq '[YamipaPlugin] Found 5 file(s) in images directory' server.log; then
|
||||
echo "Plugin did not read image directory"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Fq '[YamipaPlugin] [ImageCommandBridge] Fixed command permissions' server.log; then
|
||||
if ! grep -Fq '[YamipaPlugin] Fixed command permissions' server.log; then
|
||||
echo "Plugin did not fixed command permissions"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Fq '[YamipaPlugin] [FakeImage] Created FakeImage' server.log; then
|
||||
if ! grep -Fq '[YamipaPlugin] Created FakeImage' server.log; then
|
||||
echo "Plugin did not place the fake image"
|
||||
exit 1
|
||||
fi
|
||||
if ! grep -Fq '[YamipaPlugin] [FakeImage] Invalidated FakeImage' server.log; then
|
||||
if ! grep -Fq '[YamipaPlugin] Invalidated FakeImage' server.log; then
|
||||
echo "Plugin did not remove the fake image"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -45,50 +45,15 @@ Yamipa is ready-to-go right out of the box. By default, it creates the following
|
||||
- `images.dat`: A file holding the list and properties (e.g. coordinates) of all placed images in your server. You
|
||||
shouldn't modify its contents.
|
||||
|
||||
You can change the path of these files by creating a `config.yml` file in the plugin configuration directory.
|
||||
Here are the default configuration values if you don't specify them:
|
||||
You can change the default path of these files by creating a `config.yml` file in the plugin configuration directory:
|
||||
```yaml
|
||||
verbose: false # Set to "true" to enable more verbose logging
|
||||
animate-images: true # Set to "false" to disable GIF support
|
||||
images-path: images # Path to images directory
|
||||
cache-path: cache # Path to cache directory
|
||||
data-path: images.dat # Path to placed images database file
|
||||
allowed-paths: null # Set to a RegExp to limit accessible images to players
|
||||
max-image-dimension: 30 # Maximum width or height in blocks allowed in images
|
||||
verbose: false # Set to "true" to enable more verbose logging
|
||||
animate-images: true # Set to "false" to disable GIF support
|
||||
images-path: images # Path to images directory
|
||||
cache-path: cache # Path to cache directory
|
||||
data-path: images.dat # Path to placed images database file
|
||||
```
|
||||
|
||||
For more information on how to set a different `allowed-paths` or `max-image-dimension` value per player, see the
|
||||
[Player variables](#player-variables) section.
|
||||
|
||||
### Allowed paths
|
||||
The variable `allowed-paths` is a regular expression that determines whether a player is allowed to see or download
|
||||
an image file. If the desired path relative to the images directory matches this expression, then the player is allowed
|
||||
to continue.
|
||||
|
||||
If `allowed-paths` is an empty string ("") or null, then the player can read any image file or download to any path
|
||||
inside the images directory.
|
||||
|
||||
This regular expression must follow
|
||||
[the syntax used by Java](https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html). You can test your
|
||||
expression beforehand using an online tool like [regex101](https://regex101.com/).
|
||||
In addition, you can make use of the following special tokens:
|
||||
|
||||
- `#player#`: Player name
|
||||
- `#uuid#`: Player UUID (with hyphens)
|
||||
|
||||
For example, if you want every player in your server to have their own subdirectory for storing files that only they
|
||||
can access, plus a shared public directory, you can use the following `allowed-paths` value:
|
||||
```regexp
|
||||
^(private/#player#|public)/
|
||||
```
|
||||
|
||||
That way, the player "john" can see the image file at "private/john/something.jpg", but "jane" cannot.
|
||||
|
||||
> **IMPORTANT!**\
|
||||
> Note that these restrictions **also apply to other entities** like NPCs, command blocks or the server console.
|
||||
> However, special tokens will always match in non-player contexts (e.g., "#player#" will be interpreted as ".+").
|
||||
|
||||
### bStats
|
||||
This library uses bStats to anonymously report the number of installs. If you don't like this, feel free to
|
||||
disable it at any time by adding `enabled: false` to the
|
||||
[bStats configuration file](https://bstats.org/getting-started#:~:text=Disabling%20bStats) (it's ok, no hard feelings).
|
||||
@@ -110,17 +75,17 @@ This plugin adds the following commands:
|
||||
- Show help\
|
||||
`/image`
|
||||
- Download an image from a URL and save it with another name\
|
||||
`/image download "https://www.example.com/a/b/c/1234.jpg" "imagename.jpg"`
|
||||
`/image download "https://www.example.com/a/b/c/1234.jpg" imagename.jpg`
|
||||
- Give 10 image items to "TestPlayer" for the "test.jpg" image (3x5 blocks)\
|
||||
`/image give TestPlayer "test.jpg" 10 3 5`
|
||||
`/image give TestPlayer test.jpg 10 3 5`
|
||||
- Give 10 image items to "TestPlayer" that will not drop an image item when removed\
|
||||
`/image give TestPlayer "test.jpg" 10 3 5 -DROP`
|
||||
`/image give TestPlayer test.jpg 10 3 5 -DROP`
|
||||
- Start the dialog to place an image with a width of 3 blocks and auto height\
|
||||
`/image place "imagename.jpg" 3`
|
||||
`/image place imagename.jpg 3`
|
||||
- Start the dialog to place a 3-blocks wide and 2-blocks high image\
|
||||
`/image place "imagename.jpg" 3 2`
|
||||
`/image place imagename.jpg 3 2`
|
||||
- Start the dialog to place an image that glows in the dark\
|
||||
`/image place "imagename.jpg" 3 2 +GLOW`
|
||||
`/image place imagename.jpg 3 2 +GLOW`
|
||||
- Start the dialog to remove a placed image while keeping the original file\
|
||||
`/image remove`
|
||||
- Remove all placed images in a radius of 5 blocks around the spawn\
|
||||
@@ -158,28 +123,6 @@ You can change which roles or players are granted these commands by using a perm
|
||||
such as [LuckPerms](https://luckperms.net/) or [GroupManager](https://elgarl.github.io/GroupManager/).
|
||||
Both these plugins have been tested to work with Yamipa, although any similar one should work just fine.
|
||||
|
||||
## Player variables
|
||||
Some permission plugins like LuckPerms allow server operators to assign
|
||||
[key-value pairs](https://luckperms.net/wiki/Meta-Commands) to entities as if they were permissions.
|
||||
This is useful for granting different capabilities to different players or groups.
|
||||
|
||||
Yamipa looks for the following variables which, if found, override the default configuration value that applies to all
|
||||
players:
|
||||
|
||||
| Variable (key) | Overrides | Description |
|
||||
|:-----------------------------|:----------------------|:----------------------------------------------------------------------------------|
|
||||
| `yamipa-allowed-paths` | `allowed-paths` | Regular expression that limits which paths in the images directory are accessible |
|
||||
| `yamipa-max-image-dimension` | `max-image-dimension` | Maximum width or height of images and image items issued by this player or group |
|
||||
|
||||
For example, if you want to limit the image size to 5x5 blocks just for the "test" player, you can run this command:
|
||||
```sh
|
||||
# Using LuckPerms
|
||||
/lp user test meta set yamipa-max-image-dimension 5
|
||||
|
||||
# Using GroupManager
|
||||
/manuaddv test yamipa-max-image-dimension 5
|
||||
```
|
||||
|
||||
## Protecting areas
|
||||
In large servers, letting your players place and remove images wherever they want might not be the most sensible idea.
|
||||
For those cases, Yamipa is compatible with other Bukkit plugins that allow creating and managing world areas.
|
||||
|
||||
Generated
+193
-158
@@ -6,43 +6,43 @@
|
||||
"": {
|
||||
"name": "@yamipa/automata",
|
||||
"dependencies": {
|
||||
"mineflayer": "^4.20.1",
|
||||
"rcon-client": "^4.2.4"
|
||||
"mineflayer": "^4.14.0",
|
||||
"rcon-client": "^4.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "14.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz",
|
||||
"integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==",
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.1.0.tgz",
|
||||
"integrity": "sha512-xphmhcfl5VL+uq5//VKMwQn+wfEZLMKNpFCcMi8Ur8ej5UT166g6chBsxgMzc9xo9Y24R9FB3m/tjDiV03xMIA==",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.8.1.tgz",
|
||||
"integrity": "sha512-VcZZM+5VvCWRBTOF7SxMKaxrz+EXjntx2u5AQe7QE06e6FuPJElGBrImgNgCh5QmFaNCfVFO+3qNR7UoFD/Gfw==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.2.0.tgz",
|
||||
"integrity": "sha512-aZZ1m7OATOR9kz7+/Kl+/LZrNEj+HYaEH84XZfnBtN/9JPRazngk1XTjvAkh7W1u/O/yQrlYiiSwSAKHnNBc8A==",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "14.10.0",
|
||||
"@azure/msal-common": "14.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
"node": "18 || 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||
"version": "20.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
|
||||
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~5.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/readable-stream": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz",
|
||||
"integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.3.tgz",
|
||||
"integrity": "sha512-Z8BOzyIj3UPpn3j5DmDNKIw4wPN9N8a1d1oyteiprWq+wxdgQNC0UfFAQwWjmjyA7uoj7mvoWgxWWH66zYtm4Q==",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"safe-buffer": "~5.1.1"
|
||||
@@ -230,9 +230,9 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -273,9 +273,9 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.5",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
|
||||
"integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==",
|
||||
"version": "4.15.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.3.tgz",
|
||||
"integrity": "sha512-RZJdL9Qjd1sqNdyiVteRGV/bnWtik/+PJh1JP4kT6+x1QQMn+7ryueRys5BEueuayvSVY8CWGCisCDazeRLTuw==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
@@ -375,15 +375,26 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
|
||||
"integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw=="
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/macaddress": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.5.3.tgz",
|
||||
"integrity": "sha512-vGBKTA+jwM4KgjGZ+S/8/Mkj9rWzePyGY6jManXPGhiWu63RYwW8dKPyk5koP+8qNVhPhHgFa1y/MJ4wrjsNrg=="
|
||||
},
|
||||
"node_modules/minecraft-data": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.65.0.tgz",
|
||||
"integrity": "sha512-9K8dOrdrcpUklTdqKBtRcKur0gLZnguTvhM/1Xv52qzh8Unkto4290RJc4ueRIYo1VqN4zzQrRxO8lnqtkERDQ=="
|
||||
"version": "3.46.2",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.46.2.tgz",
|
||||
"integrity": "sha512-vunI/l5mqjU7FisB5iFxcNl6BkKLzELUvsP5P4gpUOAUtTWXEUYTnVg9jdIFOl7Y2zw3TxG2RHDa0h0ScnRCEA=="
|
||||
},
|
||||
"node_modules/minecraft-folder-path": {
|
||||
"version": "1.2.0",
|
||||
@@ -391,9 +402,9 @@
|
||||
"integrity": "sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw=="
|
||||
},
|
||||
"node_modules/minecraft-protocol": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-protocol/-/minecraft-protocol-1.47.0.tgz",
|
||||
"integrity": "sha512-IHL8faXLLIWv1O+2v2NgyKlooilu/OiSL9orI8Kqed/rZvVOrFPzs2PwMAYjpQX9gxLPhiSU19KqZ8CjfNuqhg==",
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-protocol/-/minecraft-protocol-1.44.0.tgz",
|
||||
"integrity": "sha512-sYu4fFzUKt3spPG5tAdkaB9sNQPT0sV6fyS0sS7/nxdzFfjmLhF6BLNC+32ieK4/MhgNyHtH6xusD0Bi0Roq9w==",
|
||||
"dependencies": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
"aes-js": "^3.1.2",
|
||||
@@ -402,13 +413,12 @@
|
||||
"endian-toggle": "^0.0.0",
|
||||
"lodash.get": "^4.1.2",
|
||||
"lodash.merge": "^4.3.0",
|
||||
"minecraft-data": "^3.55.0",
|
||||
"minecraft-data": "^3.37.0",
|
||||
"minecraft-folder-path": "^1.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-rsa": "^0.4.2",
|
||||
"prismarine-auth": "^2.2.0",
|
||||
"prismarine-chat": "^1.10.0",
|
||||
"prismarine-nbt": "^2.5.0",
|
||||
"prismarine-nbt": "^2.0.0",
|
||||
"prismarine-realms": "^1.2.0",
|
||||
"protodef": "^1.8.0",
|
||||
"readable-stream": "^4.1.0",
|
||||
@@ -420,12 +430,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer": {
|
||||
"version": "4.20.1",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer/-/mineflayer-4.20.1.tgz",
|
||||
"integrity": "sha512-QMMNPx4IyZE7ydAzjvGLQLCnQNUOfkk1qVZKxTTS9q3qPTAewz4GhsVUBtbQ8LSbHthe5RcQ1Sgxs4wlIma/Qw==",
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer/-/mineflayer-4.14.0.tgz",
|
||||
"integrity": "sha512-4EYzUmZNxH3Gpz3GkgO2eaR90ANb50nVhMCU2y6Rl1Ru8M6HqxID1Eg7tRgsodfAOD+AKh5SPwmPnISLcxvnOA==",
|
||||
"dependencies": {
|
||||
"minecraft-data": "^3.56.0",
|
||||
"minecraft-protocol": "^1.47.0",
|
||||
"minecraft-data": "^3.44.0",
|
||||
"minecraft-protocol": "^1.44.0",
|
||||
"prismarine-biome": "^1.1.1",
|
||||
"prismarine-block": "^1.17.0",
|
||||
"prismarine-chat": "^1.7.1",
|
||||
@@ -436,14 +446,14 @@
|
||||
"prismarine-physics": "^1.8.0",
|
||||
"prismarine-recipe": "^1.3.0",
|
||||
"prismarine-registry": "^1.5.0",
|
||||
"prismarine-windows": "^2.9.0",
|
||||
"prismarine-windows": "^2.8.0",
|
||||
"prismarine-world": "^3.6.0",
|
||||
"protodef": "^1.14.0",
|
||||
"typed-emitter": "^1.0.0",
|
||||
"vec3": "^0.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/mojangson": {
|
||||
@@ -513,9 +523,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-auth": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-auth/-/prismarine-auth-2.4.2.tgz",
|
||||
"integrity": "sha512-Cq4woGobnFYYfMBDh1WITW+Vs98toN91qAFBvBitwV7IwJaiSAh2Nl+WPUEGeg5eLBoSPpSyCVT8P2oi7Cav8g==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-auth/-/prismarine-auth-2.3.0.tgz",
|
||||
"integrity": "sha512-giKZiHwuQdpMJ7KX94UncOJqM3u+yqKIR2UI/rqmdmFUuQilV9vhlz/zehpVkvo7FE8gmZsuUMCUPhI+gtgd3A==",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^2.0.2",
|
||||
"@xboxreplay/xboxlive-auth": "^3.3.3",
|
||||
@@ -536,9 +546,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-block": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-block/-/prismarine-block-1.17.1.tgz",
|
||||
"integrity": "sha512-r1TIn/b5v77BX4a+qd+Yv+4/vZpsC/Jp5ElYxd6++2wpCnqiuxVG7BlS2Eo14vez1M2gt3qoNEl54Hr8qox/rQ==",
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-block/-/prismarine-block-1.17.0.tgz",
|
||||
"integrity": "sha512-GawzWU91pH4sLTtMvrRe2wiEbO8hgTRwViCBYoFmmqeJYLiblq4GfXOvclj3rEAEdAAVzRKCQxQHpLJhhLWssg==",
|
||||
"dependencies": {
|
||||
"minecraft-data": "^3.38.0",
|
||||
"prismarine-biome": "^1.1.0",
|
||||
@@ -549,19 +559,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-chat": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chat/-/prismarine-chat-1.10.1.tgz",
|
||||
"integrity": "sha512-XukYcuueuhDxzEXG7r8BZyt6jOObrPPB4JESCgb+/XenB9nExoSHF8eTQWWj8faKPLqm1dRQaYwFJlNBlJZJUw==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chat/-/prismarine-chat-1.8.0.tgz",
|
||||
"integrity": "sha512-hEz47GESIEP0W1KSHrK7ZSy0PqbQW7khYWenv8d5wvyAYnBs1E4KXDBJSWqyGIYinKGN9aes8v3xpQhgURK/Fg==",
|
||||
"dependencies": {
|
||||
"mojangson": "^2.0.1",
|
||||
"prismarine-item": "^1.10.0",
|
||||
"prismarine-nbt": "^2.0.0",
|
||||
"prismarine-registry": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-chunk": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chunk/-/prismarine-chunk-1.35.0.tgz",
|
||||
"integrity": "sha512-Q1lElMUle7wWxWdQjbZo3j2/dLNG325j90IcbbMmBTnHdQSWIjWFe792XOz3RVBlvrhRJEiZk38S6/eQTQ9esw==",
|
||||
"version": "1.34.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chunk/-/prismarine-chunk-1.34.0.tgz",
|
||||
"integrity": "sha512-5RuQk8w9Ot550G5mmUFZvjpHugEXozgEFXiAhynrBCHOKJ3mMQ7VvhWF2irtjx+d2t29yDAiSEiSS34vxAmODQ==",
|
||||
"dependencies": {
|
||||
"prismarine-biome": "^1.2.0",
|
||||
"prismarine-block": "^1.14.1",
|
||||
@@ -577,9 +588,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-entity": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-2.4.0.tgz",
|
||||
"integrity": "sha512-DBwjmoCX1IYAhN99KwYkk2rMArn65JHTzuuGXchr4GLWQs7UN4Pf9tELqBwNOu4r57x3RaW0+9+0sI3FvJQWzQ==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-2.3.1.tgz",
|
||||
"integrity": "sha512-HOv8l7IetHNf4hwZ7V/W4vM3GNl+e6VCtKDkH9h02TRq7jWngsggKtJV+VanCce/sNwtJUhJDjORGs728ep4MA==",
|
||||
"dependencies": {
|
||||
"prismarine-chat": "^1.4.1",
|
||||
"prismarine-item": "^1.11.2",
|
||||
@@ -597,9 +608,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-nbt": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-2.5.0.tgz",
|
||||
"integrity": "sha512-F0/8UAa9SDDnAGrBYqZc4nG8h2zj5cE2eAJU5xlDR/IsQQ3moVxkOjE3h3nMv6SbvZrvAcgX7waA/nd9LLHYdA==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-2.2.1.tgz",
|
||||
"integrity": "sha512-Mb50c58CPnuZ+qvM31DBa08tf9UumlTq1LkvpMoUpKfCuN05GZHTqCUwER3lxTSHLL0GZKghIPbYR/JQkINijQ==",
|
||||
"dependencies": {
|
||||
"protodef": "^1.9.0"
|
||||
}
|
||||
@@ -641,9 +652,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-windows": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-windows/-/prismarine-windows-2.9.0.tgz",
|
||||
"integrity": "sha512-fm4kOLjGFPov7TEJRmXHoiPabxIQrG36r2mDjlNxfkcLfMHFb3/1ML6mp4iRQa7wL0GK4DIAyiBqCWoeWDxARg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-windows/-/prismarine-windows-2.8.0.tgz",
|
||||
"integrity": "sha512-9HVhJ8tfCeRubYwQzgz8oiHNAebMJ5hDdjm45PZwrOgewaislnR2HDsbPMWiCcyWkYL7J8bVLVoSzEzv5pH98g==",
|
||||
"dependencies": {
|
||||
"prismarine-item": "^1.12.2",
|
||||
"prismarine-registry": "^1.7.0",
|
||||
@@ -662,9 +673,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-world": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-world/-/prismarine-world-3.6.3.tgz",
|
||||
"integrity": "sha512-zqdqPEYCDHzqi6hglJldEO63bOROXpbZeIdxBmoQq7o04Lf81t016LU6stFHo3E+bmp5+xU74eDFdOvzYNABkA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-world/-/prismarine-world-3.6.2.tgz",
|
||||
"integrity": "sha512-xNNo3bd8EnCMjiPbVrh3jYa1Upa8Krkb13BgO7FOOfD5ZYf+iYDZewBtDbHYWzZZB2N0JlTtimMOHRhZhDJirw==",
|
||||
"dependencies": {
|
||||
"vec3": "^0.1.7"
|
||||
},
|
||||
@@ -719,9 +730,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -744,9 +755,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rcon-client": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rcon-client/-/rcon-client-4.2.4.tgz",
|
||||
"integrity": "sha512-sTHYLbcu2qrrOX4TOD3rvcLZNs7xiAhTsnZZIvxviSpa0yeV63sPAVI0PeScn5IvhKOCMURGt1NmnQ8h/GxjtA==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/rcon-client/-/rcon-client-4.2.3.tgz",
|
||||
"integrity": "sha512-g+DEzI3O0Nl4Y3mPp6DHhSpYeiv+wTq8iKxCumFGO0/SMCsdNfx9bpT5RyHa7phR5dvspKfQK0ZYJp1YCtXNng==",
|
||||
"dependencies": {
|
||||
"typed-emitter": "^0.1.0"
|
||||
}
|
||||
@@ -757,9 +768,9 @@
|
||||
"integrity": "sha512-Tfay0l6gJMP5rkil8CzGbLthukn+9BN/VXWcABVFPjOoelJ+koW8BuPZYk+h/L+lEeIp1fSzVRiWRPIjKVjPdg=="
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
|
||||
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
|
||||
"integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
|
||||
"dependencies": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -794,9 +805,12 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"dependencies": {
|
||||
"lru-cache": "^6.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
@@ -862,9 +876,9 @@
|
||||
"integrity": "sha512-lhEx78gdTwFWG+mt6cWAZD/R6qrIj0TTBeH5xwyuDJyswLNlGe+KVlUPQ6+mx5Ld332pS0AMUTo9hIly7YsWxQ=="
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
"version": "5.25.3",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
|
||||
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
@@ -896,9 +910,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw=="
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.8.tgz",
|
||||
"integrity": "sha512-LfKrP625Bsg/Tj52YdYPsHmpsJuo+tc6fLxZxXjEo9k2xSspKlPvoYTHehykKhp1FvV9nm+XU3Ehej5/9tpDCg=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
@@ -919,6 +933,11 @@
|
||||
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz",
|
||||
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"node_modules/yggdrasil": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/yggdrasil/-/yggdrasil-1.7.0.tgz",
|
||||
@@ -931,32 +950,32 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-common": {
|
||||
"version": "14.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz",
|
||||
"integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA=="
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.1.0.tgz",
|
||||
"integrity": "sha512-xphmhcfl5VL+uq5//VKMwQn+wfEZLMKNpFCcMi8Ur8ej5UT166g6chBsxgMzc9xo9Y24R9FB3m/tjDiV03xMIA=="
|
||||
},
|
||||
"@azure/msal-node": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.8.1.tgz",
|
||||
"integrity": "sha512-VcZZM+5VvCWRBTOF7SxMKaxrz+EXjntx2u5AQe7QE06e6FuPJElGBrImgNgCh5QmFaNCfVFO+3qNR7UoFD/Gfw==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.2.0.tgz",
|
||||
"integrity": "sha512-aZZ1m7OATOR9kz7+/Kl+/LZrNEj+HYaEH84XZfnBtN/9JPRazngk1XTjvAkh7W1u/O/yQrlYiiSwSAKHnNBc8A==",
|
||||
"requires": {
|
||||
"@azure/msal-common": "14.10.0",
|
||||
"@azure/msal-common": "14.1.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "20.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
|
||||
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
|
||||
"version": "20.8.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz",
|
||||
"integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==",
|
||||
"requires": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~5.25.1"
|
||||
}
|
||||
},
|
||||
"@types/readable-stream": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz",
|
||||
"integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==",
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.3.tgz",
|
||||
"integrity": "sha512-Z8BOzyIj3UPpn3j5DmDNKIw4wPN9N8a1d1oyteiprWq+wxdgQNC0UfFAQwWjmjyA7uoj7mvoWgxWWH66zYtm4Q==",
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"safe-buffer": "~5.1.1"
|
||||
@@ -1089,9 +1108,9 @@
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.6",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA=="
|
||||
"version": "1.15.3",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
|
||||
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
@@ -1104,9 +1123,9 @@
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"jose": {
|
||||
"version": "4.15.5",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz",
|
||||
"integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg=="
|
||||
"version": "4.15.3",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.3.tgz",
|
||||
"integrity": "sha512-RZJdL9Qjd1sqNdyiVteRGV/bnWtik/+PJh1JP4kT6+x1QQMn+7ryueRys5BEueuayvSVY8CWGCisCDazeRLTuw=="
|
||||
},
|
||||
"json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
@@ -1199,15 +1218,23 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.reduce/-/lodash.reduce-4.6.0.tgz",
|
||||
"integrity": "sha512-6raRe2vxCYBhpBu+B+TtNGUzah+hQjVdu3E17wfusjyrXBka2nBS8OH/gjVZ5PvHOhWmIZTYri09Z6n/QfnNMw=="
|
||||
},
|
||||
"lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"requires": {
|
||||
"yallist": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"macaddress": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/macaddress/-/macaddress-0.5.3.tgz",
|
||||
"integrity": "sha512-vGBKTA+jwM4KgjGZ+S/8/Mkj9rWzePyGY6jManXPGhiWu63RYwW8dKPyk5koP+8qNVhPhHgFa1y/MJ4wrjsNrg=="
|
||||
},
|
||||
"minecraft-data": {
|
||||
"version": "3.65.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.65.0.tgz",
|
||||
"integrity": "sha512-9K8dOrdrcpUklTdqKBtRcKur0gLZnguTvhM/1Xv52qzh8Unkto4290RJc4ueRIYo1VqN4zzQrRxO8lnqtkERDQ=="
|
||||
"version": "3.46.2",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-3.46.2.tgz",
|
||||
"integrity": "sha512-vunI/l5mqjU7FisB5iFxcNl6BkKLzELUvsP5P4gpUOAUtTWXEUYTnVg9jdIFOl7Y2zw3TxG2RHDa0h0ScnRCEA=="
|
||||
},
|
||||
"minecraft-folder-path": {
|
||||
"version": "1.2.0",
|
||||
@@ -1215,9 +1242,9 @@
|
||||
"integrity": "sha512-qaUSbKWoOsH9brn0JQuBhxNAzTDMwrOXorwuRxdJKKKDYvZhtml+6GVCUrY5HRiEsieBEjCUnhVpDuQiKsiFaw=="
|
||||
},
|
||||
"minecraft-protocol": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-protocol/-/minecraft-protocol-1.47.0.tgz",
|
||||
"integrity": "sha512-IHL8faXLLIWv1O+2v2NgyKlooilu/OiSL9orI8Kqed/rZvVOrFPzs2PwMAYjpQX9gxLPhiSU19KqZ8CjfNuqhg==",
|
||||
"version": "1.44.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-protocol/-/minecraft-protocol-1.44.0.tgz",
|
||||
"integrity": "sha512-sYu4fFzUKt3spPG5tAdkaB9sNQPT0sV6fyS0sS7/nxdzFfjmLhF6BLNC+32ieK4/MhgNyHtH6xusD0Bi0Roq9w==",
|
||||
"requires": {
|
||||
"@types/readable-stream": "^4.0.0",
|
||||
"aes-js": "^3.1.2",
|
||||
@@ -1226,13 +1253,12 @@
|
||||
"endian-toggle": "^0.0.0",
|
||||
"lodash.get": "^4.1.2",
|
||||
"lodash.merge": "^4.3.0",
|
||||
"minecraft-data": "^3.55.0",
|
||||
"minecraft-data": "^3.37.0",
|
||||
"minecraft-folder-path": "^1.2.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-rsa": "^0.4.2",
|
||||
"prismarine-auth": "^2.2.0",
|
||||
"prismarine-chat": "^1.10.0",
|
||||
"prismarine-nbt": "^2.5.0",
|
||||
"prismarine-nbt": "^2.0.0",
|
||||
"prismarine-realms": "^1.2.0",
|
||||
"protodef": "^1.8.0",
|
||||
"readable-stream": "^4.1.0",
|
||||
@@ -1241,12 +1267,12 @@
|
||||
}
|
||||
},
|
||||
"mineflayer": {
|
||||
"version": "4.20.1",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer/-/mineflayer-4.20.1.tgz",
|
||||
"integrity": "sha512-QMMNPx4IyZE7ydAzjvGLQLCnQNUOfkk1qVZKxTTS9q3qPTAewz4GhsVUBtbQ8LSbHthe5RcQ1Sgxs4wlIma/Qw==",
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer/-/mineflayer-4.14.0.tgz",
|
||||
"integrity": "sha512-4EYzUmZNxH3Gpz3GkgO2eaR90ANb50nVhMCU2y6Rl1Ru8M6HqxID1Eg7tRgsodfAOD+AKh5SPwmPnISLcxvnOA==",
|
||||
"requires": {
|
||||
"minecraft-data": "^3.56.0",
|
||||
"minecraft-protocol": "^1.47.0",
|
||||
"minecraft-data": "^3.44.0",
|
||||
"minecraft-protocol": "^1.44.0",
|
||||
"prismarine-biome": "^1.1.1",
|
||||
"prismarine-block": "^1.17.0",
|
||||
"prismarine-chat": "^1.7.1",
|
||||
@@ -1257,7 +1283,7 @@
|
||||
"prismarine-physics": "^1.8.0",
|
||||
"prismarine-recipe": "^1.3.0",
|
||||
"prismarine-registry": "^1.5.0",
|
||||
"prismarine-windows": "^2.9.0",
|
||||
"prismarine-windows": "^2.8.0",
|
||||
"prismarine-world": "^3.6.0",
|
||||
"protodef": "^1.14.0",
|
||||
"typed-emitter": "^1.0.0",
|
||||
@@ -1310,9 +1336,9 @@
|
||||
}
|
||||
},
|
||||
"prismarine-auth": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-auth/-/prismarine-auth-2.4.2.tgz",
|
||||
"integrity": "sha512-Cq4woGobnFYYfMBDh1WITW+Vs98toN91qAFBvBitwV7IwJaiSAh2Nl+WPUEGeg5eLBoSPpSyCVT8P2oi7Cav8g==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-auth/-/prismarine-auth-2.3.0.tgz",
|
||||
"integrity": "sha512-giKZiHwuQdpMJ7KX94UncOJqM3u+yqKIR2UI/rqmdmFUuQilV9vhlz/zehpVkvo7FE8gmZsuUMCUPhI+gtgd3A==",
|
||||
"requires": {
|
||||
"@azure/msal-node": "^2.0.2",
|
||||
"@xboxreplay/xboxlive-auth": "^3.3.3",
|
||||
@@ -1330,9 +1356,9 @@
|
||||
"requires": {}
|
||||
},
|
||||
"prismarine-block": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-block/-/prismarine-block-1.17.1.tgz",
|
||||
"integrity": "sha512-r1TIn/b5v77BX4a+qd+Yv+4/vZpsC/Jp5ElYxd6++2wpCnqiuxVG7BlS2Eo14vez1M2gt3qoNEl54Hr8qox/rQ==",
|
||||
"version": "1.17.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-block/-/prismarine-block-1.17.0.tgz",
|
||||
"integrity": "sha512-GawzWU91pH4sLTtMvrRe2wiEbO8hgTRwViCBYoFmmqeJYLiblq4GfXOvclj3rEAEdAAVzRKCQxQHpLJhhLWssg==",
|
||||
"requires": {
|
||||
"minecraft-data": "^3.38.0",
|
||||
"prismarine-biome": "^1.1.0",
|
||||
@@ -1343,19 +1369,20 @@
|
||||
}
|
||||
},
|
||||
"prismarine-chat": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chat/-/prismarine-chat-1.10.1.tgz",
|
||||
"integrity": "sha512-XukYcuueuhDxzEXG7r8BZyt6jOObrPPB4JESCgb+/XenB9nExoSHF8eTQWWj8faKPLqm1dRQaYwFJlNBlJZJUw==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chat/-/prismarine-chat-1.8.0.tgz",
|
||||
"integrity": "sha512-hEz47GESIEP0W1KSHrK7ZSy0PqbQW7khYWenv8d5wvyAYnBs1E4KXDBJSWqyGIYinKGN9aes8v3xpQhgURK/Fg==",
|
||||
"requires": {
|
||||
"mojangson": "^2.0.1",
|
||||
"prismarine-item": "^1.10.0",
|
||||
"prismarine-nbt": "^2.0.0",
|
||||
"prismarine-registry": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"prismarine-chunk": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chunk/-/prismarine-chunk-1.35.0.tgz",
|
||||
"integrity": "sha512-Q1lElMUle7wWxWdQjbZo3j2/dLNG325j90IcbbMmBTnHdQSWIjWFe792XOz3RVBlvrhRJEiZk38S6/eQTQ9esw==",
|
||||
"version": "1.34.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-chunk/-/prismarine-chunk-1.34.0.tgz",
|
||||
"integrity": "sha512-5RuQk8w9Ot550G5mmUFZvjpHugEXozgEFXiAhynrBCHOKJ3mMQ7VvhWF2irtjx+d2t29yDAiSEiSS34vxAmODQ==",
|
||||
"requires": {
|
||||
"prismarine-biome": "^1.2.0",
|
||||
"prismarine-block": "^1.14.1",
|
||||
@@ -1368,9 +1395,9 @@
|
||||
}
|
||||
},
|
||||
"prismarine-entity": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-2.4.0.tgz",
|
||||
"integrity": "sha512-DBwjmoCX1IYAhN99KwYkk2rMArn65JHTzuuGXchr4GLWQs7UN4Pf9tELqBwNOu4r57x3RaW0+9+0sI3FvJQWzQ==",
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-2.3.1.tgz",
|
||||
"integrity": "sha512-HOv8l7IetHNf4hwZ7V/W4vM3GNl+e6VCtKDkH9h02TRq7jWngsggKtJV+VanCce/sNwtJUhJDjORGs728ep4MA==",
|
||||
"requires": {
|
||||
"prismarine-chat": "^1.4.1",
|
||||
"prismarine-item": "^1.11.2",
|
||||
@@ -1388,9 +1415,9 @@
|
||||
}
|
||||
},
|
||||
"prismarine-nbt": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-2.5.0.tgz",
|
||||
"integrity": "sha512-F0/8UAa9SDDnAGrBYqZc4nG8h2zj5cE2eAJU5xlDR/IsQQ3moVxkOjE3h3nMv6SbvZrvAcgX7waA/nd9LLHYdA==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-nbt/-/prismarine-nbt-2.2.1.tgz",
|
||||
"integrity": "sha512-Mb50c58CPnuZ+qvM31DBa08tf9UumlTq1LkvpMoUpKfCuN05GZHTqCUwER3lxTSHLL0GZKghIPbYR/JQkINijQ==",
|
||||
"requires": {
|
||||
"protodef": "^1.9.0"
|
||||
}
|
||||
@@ -1430,9 +1457,9 @@
|
||||
}
|
||||
},
|
||||
"prismarine-windows": {
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-windows/-/prismarine-windows-2.9.0.tgz",
|
||||
"integrity": "sha512-fm4kOLjGFPov7TEJRmXHoiPabxIQrG36r2mDjlNxfkcLfMHFb3/1ML6mp4iRQa7wL0GK4DIAyiBqCWoeWDxARg==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-windows/-/prismarine-windows-2.8.0.tgz",
|
||||
"integrity": "sha512-9HVhJ8tfCeRubYwQzgz8oiHNAebMJ5hDdjm45PZwrOgewaislnR2HDsbPMWiCcyWkYL7J8bVLVoSzEzv5pH98g==",
|
||||
"requires": {
|
||||
"prismarine-item": "^1.12.2",
|
||||
"prismarine-registry": "^1.7.0",
|
||||
@@ -1450,9 +1477,9 @@
|
||||
}
|
||||
},
|
||||
"prismarine-world": {
|
||||
"version": "3.6.3",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-world/-/prismarine-world-3.6.3.tgz",
|
||||
"integrity": "sha512-zqdqPEYCDHzqi6hglJldEO63bOROXpbZeIdxBmoQq7o04Lf81t016LU6stFHo3E+bmp5+xU74eDFdOvzYNABkA==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-world/-/prismarine-world-3.6.2.tgz",
|
||||
"integrity": "sha512-xNNo3bd8EnCMjiPbVrh3jYa1Upa8Krkb13BgO7FOOfD5ZYf+iYDZewBtDbHYWzZZB2N0JlTtimMOHRhZhDJirw==",
|
||||
"requires": {
|
||||
"vec3": "^0.1.7"
|
||||
}
|
||||
@@ -1494,9 +1521,9 @@
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
|
||||
"integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA=="
|
||||
},
|
||||
"railroad-diagrams": {
|
||||
"version": "1.0.0",
|
||||
@@ -1513,9 +1540,9 @@
|
||||
}
|
||||
},
|
||||
"rcon-client": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rcon-client/-/rcon-client-4.2.4.tgz",
|
||||
"integrity": "sha512-sTHYLbcu2qrrOX4TOD3rvcLZNs7xiAhTsnZZIvxviSpa0yeV63sPAVI0PeScn5IvhKOCMURGt1NmnQ8h/GxjtA==",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/rcon-client/-/rcon-client-4.2.3.tgz",
|
||||
"integrity": "sha512-g+DEzI3O0Nl4Y3mPp6DHhSpYeiv+wTq8iKxCumFGO0/SMCsdNfx9bpT5RyHa7phR5dvspKfQK0ZYJp1YCtXNng==",
|
||||
"requires": {
|
||||
"typed-emitter": "^0.1.0"
|
||||
},
|
||||
@@ -1528,9 +1555,9 @@
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
|
||||
"integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz",
|
||||
"integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==",
|
||||
"requires": {
|
||||
"abort-controller": "^3.0.0",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -1559,9 +1586,12 @@
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.6.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
|
||||
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w=="
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
|
||||
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"smart-buffer": {
|
||||
"version": "4.2.0",
|
||||
@@ -1605,9 +1635,9 @@
|
||||
"integrity": "sha512-lhEx78gdTwFWG+mt6cWAZD/R6qrIj0TTBeH5xwyuDJyswLNlGe+KVlUPQ6+mx5Ld332pS0AMUTo9hIly7YsWxQ=="
|
||||
},
|
||||
"undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
"version": "5.25.3",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz",
|
||||
"integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA=="
|
||||
},
|
||||
"uri-js": {
|
||||
"version": "4.4.1",
|
||||
@@ -1636,9 +1666,9 @@
|
||||
}
|
||||
},
|
||||
"vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw=="
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.8.tgz",
|
||||
"integrity": "sha512-LfKrP625Bsg/Tj52YdYPsHmpsJuo+tc6fLxZxXjEo9k2xSspKlPvoYTHehykKhp1FvV9nm+XU3Ehej5/9tpDCg=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
@@ -1659,6 +1689,11 @@
|
||||
"resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-0.4.2.tgz",
|
||||
"integrity": "sha512-/eyHVRJQCirEkSZ1agRSCwriMhwlyUcFkXD5TPVSLP+IPzjsqMVzZwdoczLp1SoQU0R3dxz1RpIK+4YNQbCVOA=="
|
||||
},
|
||||
"yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
},
|
||||
"yggdrasil": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/yggdrasil/-/yggdrasil-1.7.0.tgz",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"start": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"mineflayer": "^4.20.1",
|
||||
"rcon-client": "^4.2.4"
|
||||
"mineflayer": "^4.14.0",
|
||||
"rcon-client": "^4.2.3"
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>io.josemmo.bukkit.plugin</groupId>
|
||||
<artifactId>YamipaPlugin</artifactId>
|
||||
<version>1.3.2-SNAPSHOT-b2</version>
|
||||
<version>1.2.12</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
@@ -42,7 +42,7 @@
|
||||
<dependency>
|
||||
<groupId>org.spigotmc</groupId>
|
||||
<artifactId>spigot-api</artifactId>
|
||||
<version>1.20.6-R0.1-SNAPSHOT</version>
|
||||
<version>1.20.2-R0.1-SNAPSHOT</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<dependency>
|
||||
<groupId>com.comphenix.protocol</groupId>
|
||||
<artifactId>ProtocolLib</artifactId>
|
||||
<version>5.2.0-SNAPSHOT</version>
|
||||
<version>5.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -62,29 +62,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://central.sonatype.com/artifact/org.bstats/bstats-bukkit -->
|
||||
<!-- https://repo.maven.apache.org/maven2/org/bstats/bstats-bukkit/ -->
|
||||
<dependency>
|
||||
<groupId>org.bstats</groupId>
|
||||
<artifactId>bstats-bukkit</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://central.sonatype.com/artifact/net.luckperms/api -->
|
||||
<dependency>
|
||||
<groupId>net.luckperms</groupId>
|
||||
<artifactId>api</artifactId>
|
||||
<version>5.4</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://jitpack.io/com/github/ElgarL/groupmanager/ -->
|
||||
<dependency>
|
||||
<groupId>com.github.ElgarL</groupId>
|
||||
<artifactId>groupmanager</artifactId>
|
||||
<version>3.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://maven.enginehub.org/artifactory/repo/com/sk89q/worldguard/worldguard-bukkit/ -->
|
||||
<dependency>
|
||||
<groupId>com.sk89q.worldguard</groupId>
|
||||
@@ -97,7 +81,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.TechFortress</groupId>
|
||||
<artifactId>GriefPrevention</artifactId>
|
||||
<version>16.18.3</version>
|
||||
<version>16.18.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -105,7 +89,7 @@
|
||||
<dependency>
|
||||
<groupId>com.github.TownyAdvanced</groupId>
|
||||
<artifactId>towny</artifactId>
|
||||
<version>0.100.2.11</version>
|
||||
<version>0.99.6.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
@@ -113,15 +97,15 @@
|
||||
<dependency>
|
||||
<groupId>com.github.angeschossen</groupId>
|
||||
<artifactId>LandsAPI</artifactId>
|
||||
<version>7.1.12</version>
|
||||
<version>6.35.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- https://central.sonatype.com/artifact/org.jetbrains/annotations -->
|
||||
<!-- https://repo.maven.apache.org/maven2/org/jetbrains/annotations/ -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>24.1.0</version>
|
||||
<version>24.0.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
@@ -138,7 +122,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<version>3.2.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>shade</id>
|
||||
@@ -170,7 +154,7 @@
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<version>3.0.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.josemmo.bukkit.plugin;
|
||||
import io.josemmo.bukkit.plugin.commands.ImageCommandBridge;
|
||||
import io.josemmo.bukkit.plugin.renderer.*;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageStorage;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bstats.charts.SimplePie;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -13,29 +12,26 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.awt.Color;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class YamipaPlugin extends JavaPlugin {
|
||||
public static final int BSTATS_PLUGIN_ID = 10243;
|
||||
private static final Logger LOGGER = Logger.getLogger();
|
||||
private static @Nullable YamipaPlugin INSTANCE;
|
||||
private static YamipaPlugin instance;
|
||||
private boolean verbose;
|
||||
private @Nullable ImageStorage storage;
|
||||
private @Nullable ImageRenderer renderer;
|
||||
private @Nullable ItemService itemService;
|
||||
private @Nullable ScheduledExecutorService scheduler;
|
||||
private @Nullable Metrics metrics;
|
||||
private ImageStorage storage;
|
||||
private ImageRenderer renderer;
|
||||
private ItemService itemService;
|
||||
private ScheduledExecutorService scheduler;
|
||||
|
||||
/**
|
||||
* Get plugin instance
|
||||
* @return Plugin instance
|
||||
*/
|
||||
public static @NotNull YamipaPlugin getInstance() {
|
||||
Objects.requireNonNull(INSTANCE, "Cannot get plugin instance if plugin is not running");
|
||||
return INSTANCE;
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,7 +39,6 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
* @return Image storage instance
|
||||
*/
|
||||
public @NotNull ImageStorage getStorage() {
|
||||
Objects.requireNonNull(storage, "Cannot get storage instance if plugin is not running");
|
||||
return storage;
|
||||
}
|
||||
|
||||
@@ -52,7 +47,6 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
* @return Image renderer instance
|
||||
*/
|
||||
public @NotNull ImageRenderer getRenderer() {
|
||||
Objects.requireNonNull(renderer, "Cannot get renderer instance if plugin is not running");
|
||||
return renderer;
|
||||
}
|
||||
|
||||
@@ -61,21 +55,12 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
* @return Tasks scheduler
|
||||
*/
|
||||
public @NotNull ScheduledExecutorService getScheduler() {
|
||||
Objects.requireNonNull(scheduler, "Cannot get scheduler instance if plugin is not running");
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is verbose
|
||||
* @return Whether plugin is running in verbose mode
|
||||
*/
|
||||
public boolean isVerbose() {
|
||||
return verbose;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
INSTANCE = this;
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,7 +68,7 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
// Initialize logger
|
||||
verbose = getConfig().getBoolean("verbose", false);
|
||||
if (verbose) {
|
||||
LOGGER.info("Running on VERBOSE mode");
|
||||
info("Running on VERBOSE mode");
|
||||
}
|
||||
|
||||
// Register plugin commands
|
||||
@@ -96,23 +81,21 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
String dataPath = getConfig().getString("data-path", "images.dat");
|
||||
|
||||
// Create image storage
|
||||
String allowedPaths = getConfig().getString("allowed-paths", "");
|
||||
storage = new ImageStorage(
|
||||
basePath.resolve(imagesPath).toAbsolutePath().normalize(),
|
||||
basePath.resolve(cachePath).toAbsolutePath().normalize(),
|
||||
allowedPaths
|
||||
basePath.resolve(imagesPath).toString(),
|
||||
basePath.resolve(cachePath).toString()
|
||||
);
|
||||
try {
|
||||
storage.start();
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to initialize image storage", e);
|
||||
log(Level.SEVERE, "Failed to initialize image storage", e);
|
||||
}
|
||||
|
||||
// Create image renderer
|
||||
boolean animateImages = getConfig().getBoolean("animate-images", true);
|
||||
LOGGER.info(animateImages ? "Enabled image animation support" : "Image animation support is disabled");
|
||||
int maxImageDimension = getConfig().getInt("max-image-dimension", 30);
|
||||
renderer = new ImageRenderer(basePath.resolve(dataPath), animateImages, maxImageDimension);
|
||||
FakeImage.configure(animateImages);
|
||||
info(animateImages ? "Enabled image animation support" : "Image animation support is disabled");
|
||||
renderer = new ImageRenderer(basePath.resolve(dataPath).toString());
|
||||
renderer.start();
|
||||
|
||||
// Create image item service
|
||||
@@ -123,12 +106,12 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
scheduler = Executors.newScheduledThreadPool(6);
|
||||
|
||||
// Warm-up plugin dependencies
|
||||
LOGGER.fine("Waiting for ProtocolLib to be ready...");
|
||||
fine("Waiting for ProtocolLib to be ready...");
|
||||
scheduler.execute(() -> {
|
||||
FakeEntity.waitForProtocolLib();
|
||||
LOGGER.fine("ProtocolLib is now ready");
|
||||
fine("ProtocolLib is now ready");
|
||||
});
|
||||
LOGGER.fine("Triggered map color cache warm-up");
|
||||
fine("Triggered map color cache warm-up");
|
||||
FakeMap.pixelToIndex(Color.RED.getRGB()); // Ask for a color index to force cache generation
|
||||
|
||||
// Initialize bStats
|
||||
@@ -140,49 +123,82 @@ public class YamipaPlugin extends JavaPlugin {
|
||||
if (number >= 10) return "10-49";
|
||||
return "0-9";
|
||||
};
|
||||
metrics = new Metrics(this, BSTATS_PLUGIN_ID);
|
||||
metrics.addCustomChart(new SimplePie("animate_images", () -> animateImages ? "true" : "false"));
|
||||
Metrics metrics = new Metrics(this, BSTATS_PLUGIN_ID);
|
||||
metrics.addCustomChart(new SimplePie("animate_images", () -> FakeImage.isAnimationEnabled() ? "true" : "false"));
|
||||
metrics.addCustomChart(new SimplePie("number_of_image_files", () -> toStats.apply(storage.size())));
|
||||
metrics.addCustomChart(new SimplePie("number_of_placed_images", () -> toStats.apply(renderer.size())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Stop metrics
|
||||
if (metrics != null) {
|
||||
metrics.shutdown();
|
||||
metrics = null;
|
||||
}
|
||||
|
||||
// Stop item service
|
||||
if (itemService != null) {
|
||||
itemService.stop();
|
||||
itemService = null;
|
||||
}
|
||||
|
||||
// Stop image renderer
|
||||
if (renderer != null) {
|
||||
renderer.stop();
|
||||
renderer = null;
|
||||
}
|
||||
|
||||
// Stop image storage
|
||||
if (storage != null) {
|
||||
storage.stop();
|
||||
storage = null;
|
||||
}
|
||||
// Stop plugin components
|
||||
storage.stop();
|
||||
renderer.stop();
|
||||
itemService.stop();
|
||||
storage = null;
|
||||
renderer = null;
|
||||
itemService = null;
|
||||
|
||||
// Stop internal scheduler
|
||||
if (scheduler != null) {
|
||||
scheduler.shutdownNow();
|
||||
scheduler = null;
|
||||
}
|
||||
scheduler.shutdownNow();
|
||||
scheduler = null;
|
||||
|
||||
// Remove Bukkit listeners and tasks
|
||||
HandlerList.unregisterAll(this);
|
||||
Bukkit.getScheduler().cancelTasks(this);
|
||||
}
|
||||
|
||||
// Unlink reference to instance
|
||||
INSTANCE = null;
|
||||
/**
|
||||
* Log message
|
||||
* @param level Record level
|
||||
* @param message Message
|
||||
* @param e Throwable instance, NULL to ignore
|
||||
*/
|
||||
public void log(@NotNull Level level, @NotNull String message, @Nullable Throwable e) {
|
||||
// Fix log level
|
||||
if (level.intValue() < Level.INFO.intValue()) {
|
||||
if (!verbose) return;
|
||||
level = Level.INFO;
|
||||
}
|
||||
|
||||
// Proxy record to real logger
|
||||
if (e == null) {
|
||||
getLogger().log(level, message);
|
||||
} else {
|
||||
getLogger().log(level, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message
|
||||
* @param level Record level
|
||||
* @param message Message
|
||||
*/
|
||||
public void log(@NotNull Level level, @NotNull String message) {
|
||||
log(level, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
* @param message Message
|
||||
*/
|
||||
public void warning(@NotNull String message) {
|
||||
log(Level.WARNING, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
* @param message Message
|
||||
*/
|
||||
public void info(@NotNull String message) {
|
||||
log(Level.INFO, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log fine message
|
||||
* @param message Message
|
||||
*/
|
||||
public void fine(@NotNull String message) {
|
||||
log(Level.FINE, message);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import org.bukkit.ChatColor;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -20,8 +19,8 @@ public class Command {
|
||||
private final String name;
|
||||
private final List<Argument> arguments = new ArrayList<>();
|
||||
private Predicate<CommandSender> requirementHandler = __ -> true;
|
||||
private @Nullable BiConsumer<CommandSender, Object[]> executesHandler;
|
||||
private @Nullable BiConsumer<Player, Object[]> executesPlayerHandler;
|
||||
private BiConsumer<CommandSender, Object[]> executesHandler = null;
|
||||
private BiConsumer<Player, Object[]> executesPlayerHandler = null;
|
||||
private final List<Command> subcommands = new ArrayList<>();
|
||||
|
||||
/**
|
||||
@@ -83,7 +82,6 @@ public class Command {
|
||||
* @param handler Command handler
|
||||
* @return This instance
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public @NotNull Command executesPlayer(@NotNull BiConsumer<Player, Object[]> handler) {
|
||||
executesPlayerHandler = handler;
|
||||
return this;
|
||||
@@ -129,12 +127,7 @@ public class Command {
|
||||
|
||||
// Chain command elements from the bottom-up
|
||||
if (argIndex < arguments.size()) {
|
||||
Argument argument = arguments.get(argIndex);
|
||||
ArgumentBuilder argumentBuilder = argument.build().suggests((ctx, builder) -> {
|
||||
CommandSender sender = Internals.getBukkitSender(ctx.getSource());
|
||||
return argument.suggest(sender, builder);
|
||||
});
|
||||
parent.then(buildElement(argumentBuilder, argIndex+1)).executes(ctx -> {
|
||||
parent.then(buildElement(arguments.get(argIndex).build(), argIndex+1)).executes(ctx -> {
|
||||
CommandSender sender = Internals.getBukkitSender(ctx.getSource());
|
||||
sender.sendMessage(ChatColor.RED + "Missing required arguments");
|
||||
return 1;
|
||||
|
||||
@@ -5,8 +5,6 @@ import io.josemmo.bukkit.plugin.renderer.FakeImage;
|
||||
import io.josemmo.bukkit.plugin.renderer.ImageRenderer;
|
||||
import io.josemmo.bukkit.plugin.renderer.ItemService;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageFile;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageStorage;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import io.josemmo.bukkit.plugin.utils.Permissions;
|
||||
import io.josemmo.bukkit.plugin.utils.SelectBlockTask;
|
||||
import io.josemmo.bukkit.plugin.utils.ActionBar;
|
||||
@@ -25,15 +23,13 @@ import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class ImageCommand {
|
||||
private static final int ITEMS_PER_PAGE = 9;
|
||||
private static final int MAX_PATH_DEPTH = 10;
|
||||
private static final Logger LOGGER = Logger.getLogger("ImageCommand");
|
||||
public static final int ITEMS_PER_PAGE = 9;
|
||||
|
||||
public static void showHelp(@NotNull CommandSender s, @NotNull String commandName) {
|
||||
String cmd = "/" + commandName;
|
||||
@@ -66,9 +62,8 @@ public class ImageCommand {
|
||||
}
|
||||
|
||||
public static void listImages(@NotNull CommandSender sender, int page) {
|
||||
ImageStorage storage = YamipaPlugin.getInstance().getStorage();
|
||||
List<String> filenames = storage.getFilenames(sender);
|
||||
int numOfImages = filenames.size();
|
||||
String[] filenames = YamipaPlugin.getInstance().getStorage().getAllFilenames();
|
||||
int numOfImages = filenames.length;
|
||||
|
||||
// Are there any images available?
|
||||
if (numOfImages == 0) {
|
||||
@@ -90,42 +85,24 @@ public class ImageCommand {
|
||||
sender.sendMessage("=== Page " + page + " out of " + maxPage + " ===");
|
||||
}
|
||||
for (int i=firstImageIndex; i<stopImageIndex; ++i) {
|
||||
sender.sendMessage(ChatColor.GOLD + filenames.get(i));
|
||||
sender.sendMessage("" + ChatColor.GOLD + filenames[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public static void downloadImage(@NotNull CommandSender sender, @NotNull String rawUrl, @NotNull String filename) {
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
ImageStorage storage = plugin.getStorage();
|
||||
Path basePath = storage.getBasePath();
|
||||
|
||||
// Resolve destination path
|
||||
Path destPath;
|
||||
try {
|
||||
destPath = basePath.resolve(filename).normalize();
|
||||
} catch (InvalidPathException e) {
|
||||
sender.sendMessage(ChatColor.RED + "Malformed destination path");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate destination file
|
||||
if (!destPath.startsWith(basePath)) {
|
||||
Path basePath = Paths.get(plugin.getStorage().getBasePath());
|
||||
Path destPath = basePath.resolve(filename);
|
||||
if (!destPath.getParent().equals(basePath)) {
|
||||
sender.sendMessage(ChatColor.RED + "Not a valid destination filename");
|
||||
return;
|
||||
}
|
||||
if (!storage.isPathAllowed(destPath, sender)) {
|
||||
sender.sendMessage(ChatColor.RED + "Not allowed to download a file here");
|
||||
return;
|
||||
}
|
||||
if (destPath.toFile().exists()) {
|
||||
sender.sendMessage(ChatColor.RED + "There's already a file with that name");
|
||||
return;
|
||||
}
|
||||
int depth = destPath.getNameCount() - basePath.getNameCount();
|
||||
if (depth > MAX_PATH_DEPTH) {
|
||||
sender.sendMessage(ChatColor.RED + "Destination path has too many directories");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and fix remote URL
|
||||
URL url;
|
||||
@@ -172,7 +149,6 @@ public class ImageCommand {
|
||||
|
||||
// Download file
|
||||
sender.sendMessage("Downloading file...");
|
||||
Files.createDirectories(destPath.getParent());
|
||||
Files.copy(conn.getInputStream(), destPath);
|
||||
|
||||
// Validate downloaded file
|
||||
@@ -184,10 +160,10 @@ public class ImageCommand {
|
||||
sender.sendMessage(ChatColor.GREEN + "Done!");
|
||||
} catch (IOException e) {
|
||||
sender.sendMessage(ChatColor.RED + "An error occurred trying to download the remote file");
|
||||
LOGGER.warning("Failed to download file from \"" + finalUrl + "\": " + e.getClass().getName());
|
||||
plugin.warning("Failed to download file from \"" + finalUrl + "\": " + e.getClass().getName());
|
||||
} catch (IllegalArgumentException e) {
|
||||
if (Files.exists(destPath) && !destPath.toFile().delete()) {
|
||||
LOGGER.warning("Failed to delete corrupted file \"" + destPath + "\"");
|
||||
plugin.warning("Failed to delete corrupted file \"" + destPath + "\"");
|
||||
}
|
||||
sender.sendMessage(ChatColor.RED + e.getMessage());
|
||||
}
|
||||
@@ -207,11 +183,13 @@ public class ImageCommand {
|
||||
player.sendMessage(ChatColor.RED + "The requested file is not a valid image");
|
||||
return;
|
||||
}
|
||||
final int finalHeight = (height == 0) ? FakeImage.getProportionalHeight(sizeInPixels, player, width) : height;
|
||||
final int finalHeight = (height == 0) ? FakeImage.getProportionalHeight(sizeInPixels, width) : height;
|
||||
|
||||
// Ask player where to place image
|
||||
SelectBlockTask task = new SelectBlockTask(player);
|
||||
task.onSuccess((location, face) -> placeImage(player, image, width, finalHeight, flags, location, face));
|
||||
task.onSuccess((location, face) -> {
|
||||
placeImage(player, image, width, finalHeight, flags, location, face);
|
||||
});
|
||||
task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image placing canceled"));
|
||||
task.run("Right click a block to continue");
|
||||
}
|
||||
@@ -229,7 +207,7 @@ public class ImageCommand {
|
||||
|
||||
// Create new fake image instance
|
||||
Rotation rotation = FakeImage.getRotationFromPlayerEyesight(face, player.getEyeLocation());
|
||||
FakeImage fakeImage = new FakeImage(image.getFilename(), location, face, rotation,
|
||||
FakeImage fakeImage = new FakeImage(image.getName(), location, face, rotation,
|
||||
width, height, new Date(), player, flags);
|
||||
|
||||
// Make sure image can be placed
|
||||
@@ -303,7 +281,7 @@ public class ImageCommand {
|
||||
|
||||
// Get images in area
|
||||
Set<FakeImage> images = renderer.getImages(
|
||||
Objects.requireNonNull(origin.getWorld()),
|
||||
origin.getWorld(),
|
||||
origin.getBlockX()-radius+1,
|
||||
origin.getBlockX()+radius-1,
|
||||
origin.getBlockZ()-radius+1,
|
||||
@@ -468,7 +446,7 @@ public class ImageCommand {
|
||||
return;
|
||||
}
|
||||
if (height == 0) {
|
||||
height = FakeImage.getProportionalHeight(sizeInPixels, sender, width);
|
||||
height = FakeImage.getProportionalHeight(sizeInPixels, width);
|
||||
}
|
||||
|
||||
// Create item stack
|
||||
|
||||
@@ -7,7 +7,6 @@ import io.josemmo.bukkit.plugin.commands.arguments.*;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeImage;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageFile;
|
||||
import io.josemmo.bukkit.plugin.utils.Internals;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
@@ -16,9 +15,8 @@ import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class ImageCommandBridge {
|
||||
private static final String COMMAND_NAME = "yamipa";
|
||||
private static final String[] COMMAND_ALIASES = new String[] {"image", "images"};
|
||||
private static final Logger LOGGER = Logger.getLogger("ImageCommandBridge");
|
||||
public static final String COMMAND_NAME = "yamipa";
|
||||
public static final String[] COMMAND_ALIASES = new String[] {"image", "images"};
|
||||
|
||||
/**
|
||||
* Register command
|
||||
@@ -44,7 +42,7 @@ public class ImageCommandBridge {
|
||||
);
|
||||
dispatcher.getRoot().addChild(aliasNode);
|
||||
}
|
||||
LOGGER.fine("Registered plugin command and aliases");
|
||||
plugin.fine("Registered plugin command and aliases");
|
||||
|
||||
// Fix "minecraft.command.*" permissions
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
||||
@@ -52,11 +50,10 @@ public class ImageCommandBridge {
|
||||
for (String alias : COMMAND_ALIASES) {
|
||||
fixPermissions(alias);
|
||||
}
|
||||
LOGGER.fine("Fixed command permissions");
|
||||
plugin.fine("Fixed command permissions");
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
private static @NotNull Command getRootCommand() {
|
||||
Command root = new Command(COMMAND_NAME);
|
||||
|
||||
@@ -120,8 +117,8 @@ public class ImageCommandBridge {
|
||||
.withArgument(new OnlinePlayerArgument("player"))
|
||||
.withArgument(new ImageFileArgument("filename"))
|
||||
.withArgument(new IntegerArgument("amount", 1, 64))
|
||||
.withArgument(new ImageDimensionArgument("width"))
|
||||
.withArgument(new ImageDimensionArgument("height"))
|
||||
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
|
||||
.withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
|
||||
.withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_GIVE_FLAGS))
|
||||
.executes((sender, args) -> {
|
||||
ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
|
||||
@@ -132,8 +129,8 @@ public class ImageCommandBridge {
|
||||
.withArgument(new OnlinePlayerArgument("player"))
|
||||
.withArgument(new ImageFileArgument("filename"))
|
||||
.withArgument(new IntegerArgument("amount", 1, 64))
|
||||
.withArgument(new ImageDimensionArgument("width"))
|
||||
.withArgument(new ImageDimensionArgument("height"))
|
||||
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
|
||||
.withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
|
||||
.executes((sender, args) -> {
|
||||
ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
|
||||
(int) args[4], (int) args[5], FakeImage.DEFAULT_GIVE_FLAGS);
|
||||
@@ -143,7 +140,7 @@ public class ImageCommandBridge {
|
||||
.withArgument(new OnlinePlayerArgument("player"))
|
||||
.withArgument(new ImageFileArgument("filename"))
|
||||
.withArgument(new IntegerArgument("amount", 1, 64))
|
||||
.withArgument(new ImageDimensionArgument("width"))
|
||||
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
|
||||
.executes((sender, args) -> {
|
||||
ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
|
||||
(int) args[4], 0, FakeImage.DEFAULT_GIVE_FLAGS);
|
||||
@@ -167,8 +164,8 @@ public class ImageCommandBridge {
|
||||
root.addSubcommand("place")
|
||||
.withPermission("yamipa.command.place", "yamipa.place")
|
||||
.withArgument(new ImageFileArgument("filename"))
|
||||
.withArgument(new ImageDimensionArgument("width"))
|
||||
.withArgument(new ImageDimensionArgument("height"))
|
||||
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
|
||||
.withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
|
||||
.withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_PLACE_FLAGS))
|
||||
.executesPlayer((player, args) -> {
|
||||
ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3], (int) args[4]);
|
||||
@@ -176,8 +173,8 @@ public class ImageCommandBridge {
|
||||
root.addSubcommand("place")
|
||||
.withPermission("yamipa.command.place", "yamipa.place")
|
||||
.withArgument(new ImageFileArgument("filename"))
|
||||
.withArgument(new ImageDimensionArgument("width"))
|
||||
.withArgument(new ImageDimensionArgument("height"))
|
||||
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
|
||||
.withArgument(new IntegerArgument("height", 1, FakeImage.MAX_DIMENSION))
|
||||
.executesPlayer((player, args) -> {
|
||||
ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3],
|
||||
FakeImage.DEFAULT_PLACE_FLAGS);
|
||||
@@ -185,7 +182,7 @@ public class ImageCommandBridge {
|
||||
root.addSubcommand("place")
|
||||
.withPermission("yamipa.command.place", "yamipa.place")
|
||||
.withArgument(new ImageFileArgument("filename"))
|
||||
.withArgument(new ImageDimensionArgument("width"))
|
||||
.withArgument(new IntegerArgument("width", 1, FakeImage.MAX_DIMENSION))
|
||||
.executesPlayer((player, args) -> {
|
||||
ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], 0,
|
||||
FakeImage.DEFAULT_PLACE_FLAGS);
|
||||
|
||||
@@ -4,11 +4,8 @@ import com.mojang.brigadier.LiteralMessage;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public abstract class Argument {
|
||||
protected final String name;
|
||||
@@ -31,20 +28,10 @@ public abstract class Argument {
|
||||
|
||||
/**
|
||||
* Build argument
|
||||
* @return Argument builder instance
|
||||
* @return Required Argument Builder instance
|
||||
*/
|
||||
public abstract @NotNull RequiredArgumentBuilder<?, ?> build();
|
||||
|
||||
/**
|
||||
* Suggest argument values
|
||||
* @param sender Command sender
|
||||
* @param builder Suggestions builder instance
|
||||
* @return Suggestions
|
||||
*/
|
||||
public @NotNull CompletableFuture<Suggestions> suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) {
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse argument value
|
||||
* @param sender Command sender
|
||||
@@ -55,11 +42,6 @@ public abstract class Argument {
|
||||
return rawValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new syntax exception
|
||||
* @param message Message to show
|
||||
* @return Syntax exception
|
||||
*/
|
||||
protected @NotNull CommandSyntaxException newException(@NotNull String message) {
|
||||
return new SimpleCommandExceptionType(new LiteralMessage(message)).create();
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
package io.josemmo.bukkit.plugin.commands.arguments;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeImage;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class ImageDimensionArgument extends IntegerArgument {
|
||||
/**
|
||||
* Dimension Argument constructor
|
||||
* @param name Argument name
|
||||
*/
|
||||
public ImageDimensionArgument(@NotNull String name) {
|
||||
super(name, 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
|
||||
int maxDimension = FakeImage.getMaxImageDimension(sender);
|
||||
int value = (int) rawValue;
|
||||
if (value > maxDimension) {
|
||||
throw newException("Image cannot be larger than " + maxDimension + "x" + maxDimension);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
package io.josemmo.bukkit.plugin.commands.arguments;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageFile;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageStorage;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -21,21 +21,26 @@ public class ImageFileArgument extends StringArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CompletableFuture<Suggestions> suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) {
|
||||
for (String filename : YamipaPlugin.getInstance().getStorage().getFilenames(sender)) {
|
||||
builder.suggest(StringArgumentType.escapeIfRequired(filename));
|
||||
}
|
||||
return builder.buildFuture();
|
||||
public @NotNull RequiredArgumentBuilder<?, ?> build() {
|
||||
return super.build().suggests(this::getSuggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
|
||||
String filename = (String) rawValue;
|
||||
ImageStorage storage = YamipaPlugin.getInstance().getStorage();
|
||||
ImageFile imageFile = storage.get(filename);
|
||||
if (imageFile == null || !storage.isPathAllowed(filename, sender)) {
|
||||
ImageFile imageFile = YamipaPlugin.getInstance().getStorage().get((String) rawValue);
|
||||
if (imageFile == null) {
|
||||
throw newException("Image file does not exist");
|
||||
}
|
||||
return imageFile;
|
||||
}
|
||||
|
||||
private @NotNull CompletableFuture<Suggestions> getSuggestions(
|
||||
@NotNull CommandContext<?> ctx,
|
||||
@NotNull SuggestionsBuilder builder
|
||||
) {
|
||||
for (String filename : YamipaPlugin.getInstance().getStorage().getAllFilenames()) {
|
||||
builder.suggest(filename);
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package io.josemmo.bukkit.plugin.commands.arguments;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
@@ -27,11 +28,13 @@ public class ImageFlagsArgument extends Argument {
|
||||
|
||||
@Override
|
||||
public @NotNull RequiredArgumentBuilder<?, ?> build() {
|
||||
return RequiredArgumentBuilder.argument(name, StringArgumentType.greedyString());
|
||||
return RequiredArgumentBuilder.argument(name, StringArgumentType.greedyString()).suggests(this::getSuggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CompletableFuture<Suggestions> suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) {
|
||||
private @NotNull CompletableFuture<Suggestions> getSuggestions(
|
||||
@NotNull CommandContext<?> ctx,
|
||||
@NotNull SuggestionsBuilder builder
|
||||
) {
|
||||
String input = builder.getRemaining().replaceAll("[^A-Z+\\-,]", "");
|
||||
int lastIndex = Collections.max(
|
||||
Arrays.asList(input.lastIndexOf(","), input.lastIndexOf("+"), input.lastIndexOf("-"))
|
||||
|
||||
+12
-3
@@ -1,5 +1,7 @@
|
||||
package io.josemmo.bukkit.plugin.commands.arguments;
|
||||
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
@@ -21,9 +23,8 @@ public class OnlinePlayerArgument extends StringArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CompletableFuture<Suggestions> suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) {
|
||||
getAllowedValues().keySet().forEach(builder::suggest);
|
||||
return builder.buildFuture();
|
||||
public @NotNull RequiredArgumentBuilder<?, ?> build() {
|
||||
return super.build().suggests(this::getSuggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -35,6 +36,14 @@ public class OnlinePlayerArgument extends StringArgument {
|
||||
return player;
|
||||
}
|
||||
|
||||
private @NotNull CompletableFuture<Suggestions> getSuggestions(
|
||||
@NotNull CommandContext<?> ctx,
|
||||
@NotNull SuggestionsBuilder builder
|
||||
) {
|
||||
getAllowedValues().keySet().forEach(builder::suggest);
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private @NotNull Map<String, Player> getAllowedValues() {
|
||||
Map<String, Player> values = new HashMap<>();
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.josemmo.bukkit.plugin.commands.arguments;
|
||||
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
@@ -22,9 +24,8 @@ public class PlacedByArgument extends StringArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CompletableFuture<Suggestions> suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) {
|
||||
getAllowedValues().keySet().forEach(builder::suggest);
|
||||
return builder.buildFuture();
|
||||
public @NotNull RequiredArgumentBuilder<?, ?> build() {
|
||||
return super.build().suggests(this::getSuggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -36,6 +37,14 @@ public class PlacedByArgument extends StringArgument {
|
||||
return player;
|
||||
}
|
||||
|
||||
private @NotNull CompletableFuture<Suggestions> getSuggestions(
|
||||
@NotNull CommandContext<?> ctx,
|
||||
@NotNull SuggestionsBuilder builder
|
||||
) {
|
||||
getAllowedValues().keySet().forEach(builder::suggest);
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
private @NotNull Map<String, OfflinePlayer> getAllowedValues() {
|
||||
Map<String, OfflinePlayer> values = new HashMap<>();
|
||||
ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.josemmo.bukkit.plugin.commands.arguments;
|
||||
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.context.CommandContext;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
@@ -19,11 +21,8 @@ public class WorldArgument extends StringArgument {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull CompletableFuture<Suggestions> suggest(@NotNull CommandSender sender, @NotNull SuggestionsBuilder builder) {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
builder.suggest(world.getName());
|
||||
}
|
||||
return builder.buildFuture();
|
||||
public @NotNull RequiredArgumentBuilder<?, ?> build() {
|
||||
return super.build().suggests(this::getSuggestions);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -34,4 +33,14 @@ public class WorldArgument extends StringArgument {
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
private @NotNull CompletableFuture<Suggestions> getSuggestions(
|
||||
@NotNull CommandContext<?> ctx,
|
||||
@NotNull SuggestionsBuilder builder
|
||||
) {
|
||||
for (World world : Bukkit.getWorlds()) {
|
||||
builder.suggest(world.getName());
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
}
|
||||
@@ -8,32 +8,31 @@ import com.comphenix.protocol.injector.player.PlayerInjectionHandler;
|
||||
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import io.josemmo.bukkit.plugin.utils.Internals;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public abstract class FakeEntity {
|
||||
private static final Logger LOGGER = Logger.getLogger("FakeEntity");
|
||||
private static final ProtocolManager CONNECTION = ProtocolLibrary.getProtocolManager();
|
||||
private static @Nullable PlayerInjectionHandler PLAYER_INJECTION_HANDLER;
|
||||
private static boolean READY = false;
|
||||
protected static final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private static final ProtocolManager connection = ProtocolLibrary.getProtocolManager();
|
||||
private static PlayerInjectionHandler playerInjectionHandler = null;
|
||||
private static boolean ready = false;
|
||||
|
||||
static {
|
||||
try {
|
||||
for (Field field : CONNECTION.getClass().getDeclaredFields()) {
|
||||
for (Field field : connection.getClass().getDeclaredFields()) {
|
||||
if (field.getType().equals(PlayerInjectionHandler.class)) {
|
||||
field.setAccessible(true);
|
||||
PLAYER_INJECTION_HANDLER = (PlayerInjectionHandler) field.get(CONNECTION);
|
||||
playerInjectionHandler = (PlayerInjectionHandler) field.get(connection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (PLAYER_INJECTION_HANDLER == null) {
|
||||
if (playerInjectionHandler == null) {
|
||||
throw new RuntimeException("No valid candidate field found in ProtocolManager");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to get PlayerInjectionHandler from ProtocolLib", e);
|
||||
plugin.log(Level.SEVERE, "Failed to get PlayerInjectionHandler from ProtocolLib", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +42,7 @@ public abstract class FakeEntity {
|
||||
* NOTE: Will wait synchronously, blocking the invoker thread
|
||||
*/
|
||||
public static synchronized void waitForProtocolLib() {
|
||||
if (READY) {
|
||||
if (ready) {
|
||||
// ProtocolLib is ready
|
||||
return;
|
||||
}
|
||||
@@ -52,7 +51,7 @@ public abstract class FakeEntity {
|
||||
while (true) {
|
||||
try {
|
||||
WrappedDataWatcher.Registry.get(Byte.class);
|
||||
READY = true;
|
||||
ready = true;
|
||||
break;
|
||||
} catch (Exception e) {
|
||||
if (++retry > 20) {
|
||||
@@ -85,15 +84,15 @@ public abstract class FakeEntity {
|
||||
*/
|
||||
protected static void tryToSendPacket(@NotNull Player player, @NotNull PacketContainer packet) {
|
||||
try {
|
||||
if (PLAYER_INJECTION_HANDLER == null) { // Use single-threaded packet sending if reflection failed
|
||||
CONNECTION.sendServerPacket(player, packet);
|
||||
if (playerInjectionHandler == null) { // Use single-threaded packet sending if reflection failed
|
||||
connection.sendServerPacket(player, packet);
|
||||
} else { // Use non-blocking packet sending if available (faster, the expected case)
|
||||
PLAYER_INJECTION_HANDLER.sendServerPacket(player, packet, null, false);
|
||||
playerInjectionHandler.sendServerPacket(player, packet, null, false);
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
// Server is shutting down and cannot send the packet, ignore
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to send FakeEntity packet", e);
|
||||
plugin.log(Level.SEVERE, "Failed to send FakeEntity packet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +118,6 @@ public abstract class FakeEntity {
|
||||
* @param callback Callback to execute
|
||||
*/
|
||||
protected static void tryToRunAsyncTask(@NotNull Runnable callback) {
|
||||
YamipaPlugin.getInstance().getScheduler().execute(callback);
|
||||
plugin.getScheduler().execute(callback);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
package io.josemmo.bukkit.plugin.renderer;
|
||||
|
||||
import com.comphenix.protocol.events.PacketContainer;
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import io.josemmo.bukkit.plugin.storage.CachedMapsFile;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageFile;
|
||||
import io.josemmo.bukkit.plugin.utils.DirectionUtils;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import io.josemmo.bukkit.plugin.utils.Permissions;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.Rotation;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.util.Vector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -24,9 +19,7 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class FakeImage extends FakeEntity {
|
||||
private static final Logger LOGGER = Logger.getLogger("FakeImage");
|
||||
|
||||
// Image constants
|
||||
public static final int MAX_DIMENSION = 30; // In blocks
|
||||
public static final int MAX_STEPS = 500; // For animated images
|
||||
public static final int MIN_DELAY = 1; // Minimum step delay in 50ms intervals (50ms / 50ms)
|
||||
public static final int MAX_DELAY = 50; // Maximum step delay in 50ms intervals (5000ms / 50ms)
|
||||
@@ -52,7 +45,7 @@ public class FakeImage extends FakeEntity {
|
||||
private final int flags;
|
||||
private final BiFunction<Integer, Integer, Vector> getLocationVector;
|
||||
private final Set<Player> observingPlayers = new HashSet<>();
|
||||
private @Nullable Runnable onLoadedListener = null;
|
||||
private Runnable onLoadedListener = null;
|
||||
|
||||
// Generated values
|
||||
private boolean loading = false;
|
||||
@@ -61,9 +54,26 @@ public class FakeImage extends FakeEntity {
|
||||
private int numOfSteps = -1; // Total number of animation steps
|
||||
|
||||
// Animation task attributes
|
||||
private @Nullable ScheduledFuture<?> task;
|
||||
private static boolean animateImages = false;
|
||||
private ScheduledFuture<?> task;
|
||||
private int currentStep = -1; // Current animation step
|
||||
|
||||
/**
|
||||
* Configure class
|
||||
* @param animImages Animate images
|
||||
*/
|
||||
public static void configure(boolean animImages) {
|
||||
animateImages = animImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is animation enabled
|
||||
* @return Is animation enabled
|
||||
*/
|
||||
public static boolean isAnimationEnabled() {
|
||||
return animateImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image rotation from player eyesight
|
||||
* @param face Image block face
|
||||
@@ -90,36 +100,16 @@ public class FakeImage extends FakeEntity {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum image dimension
|
||||
* @param sender Sender instance
|
||||
* @return Maximum image dimension in blocks
|
||||
*/
|
||||
public static int getMaxImageDimension(@NotNull CommandSender sender) {
|
||||
if (sender instanceof Player) {
|
||||
String rawValue = Permissions.getVariable("yamipa-max-image-dimension", (Player) sender);
|
||||
if (rawValue != null) {
|
||||
try {
|
||||
return Integer.parseInt(rawValue);
|
||||
} catch (NumberFormatException __) {
|
||||
LOGGER.warning("Max. image dimension for " + sender + " is not a valid integer: \"" + rawValue + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
return YamipaPlugin.getInstance().getRenderer().getMaxImageDimension();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get proportional height
|
||||
* @param sizeInPixels Image file dimension in pixels
|
||||
* @param sender Sender instance
|
||||
* @param width Desired width in blocks
|
||||
* @return Height in blocks (capped at maximum image dimension for sender)
|
||||
* @return Height in blocks (capped at <code>FakeImage.MAX_DIMENSION</code>)
|
||||
*/
|
||||
public static int getProportionalHeight(@NotNull Dimension sizeInPixels, @NotNull CommandSender sender, int width) {
|
||||
public static int getProportionalHeight(@NotNull Dimension sizeInPixels, int width) {
|
||||
float imageRatio = (float) sizeInPixels.height / sizeInPixels.width;
|
||||
int height = Math.round(width * imageRatio);
|
||||
height = Math.min(height, getMaxImageDimension(sender));
|
||||
height = Math.min(height, MAX_DIMENSION);
|
||||
return height;
|
||||
}
|
||||
|
||||
@@ -187,7 +177,7 @@ public class FakeImage extends FakeEntity {
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.fine("Created FakeImage#(" + location + "," + face + ") from ImageFile#(" + filename + ")");
|
||||
plugin.fine("Created FakeImage#(" + location + "," + face + ") from ImageFile#(" + filename + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +193,7 @@ public class FakeImage extends FakeEntity {
|
||||
* @return Image file instance or NULL if not found
|
||||
*/
|
||||
public @Nullable ImageFile getFile() {
|
||||
return YamipaPlugin.getInstance().getStorage().get(filename);
|
||||
return plugin.getStorage().get(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,7 +310,6 @@ public class FakeImage extends FakeEntity {
|
||||
* @param face Block face
|
||||
* @return TRUE for contained, FALSE otherwise
|
||||
*/
|
||||
@SuppressWarnings("RedundantIfStatement")
|
||||
public boolean contains(@NotNull Location location, @NotNull BlockFace face) {
|
||||
// Is point facing the same plane as the image?
|
||||
if (face != this.face) {
|
||||
@@ -361,18 +350,18 @@ public class FakeImage extends FakeEntity {
|
||||
*/
|
||||
private void load() {
|
||||
ImageFile file = getFile();
|
||||
|
||||
// Get maps to use
|
||||
FakeMap[][][] maps;
|
||||
FakeMapsContainer container;
|
||||
if (file == null) {
|
||||
maps = FakeMap.getErrorMatrix(width, height);
|
||||
LOGGER.warning("File \"" + filename + "\" does not exist");
|
||||
container = FakeMap.getErrorMatrix(width, height);
|
||||
plugin.warning("File \"" + filename + "\" does not exist");
|
||||
} else {
|
||||
CachedMapsFile cachedMapsFile = file.getMapsAndSubscribe(this);
|
||||
maps = cachedMapsFile.getMaps();
|
||||
delay = cachedMapsFile.getDelay();
|
||||
container = file.getMapsAndSubscribe(this);
|
||||
}
|
||||
|
||||
// Extract data from container
|
||||
FakeMap[][][] maps = container.getFakeMaps();
|
||||
numOfSteps = maps[0][0].length;
|
||||
delay = container.getDelay();
|
||||
|
||||
// Generate frames
|
||||
FakeItemFrame[] newFrames = new FakeItemFrame[width*height];
|
||||
@@ -386,16 +375,14 @@ public class FakeImage extends FakeEntity {
|
||||
frames = newFrames;
|
||||
|
||||
// Start animation task (if needed)
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
boolean isAnimationEnabled = plugin.getRenderer().isAnimationEnabled();
|
||||
if (isAnimationEnabled && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) {
|
||||
if (animateImages && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) {
|
||||
task = plugin.getScheduler().scheduleAtFixedRate(
|
||||
this::nextStep,
|
||||
0,
|
||||
delay*50L,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
LOGGER.fine("Spawned animation task for FakeImage#(" + location + "," + face + ")");
|
||||
plugin.fine("Spawned animation task for FakeImage#(" + location + "," + face + ")");
|
||||
}
|
||||
|
||||
// Notify listener
|
||||
@@ -410,7 +397,7 @@ public class FakeImage extends FakeEntity {
|
||||
* @param player Player instance
|
||||
*/
|
||||
public void spawn(@NotNull Player player) {
|
||||
LOGGER.fine("Received request to spawn FakeImage#(" + location + "," + face + ") for Player#" + player.getName());
|
||||
plugin.fine("Received request to spawn FakeImage#(" + location + "," + face + ") for Player#" + player.getName());
|
||||
|
||||
// Send pixels if instance is already loaded
|
||||
if (frames != null) {
|
||||
@@ -452,7 +439,7 @@ public class FakeImage extends FakeEntity {
|
||||
for (FakeItemFrame frame : frames) {
|
||||
packets.add(frame.getSpawnPacket());
|
||||
packets.addAll(frame.getRenderPackets(player, 0));
|
||||
LOGGER.fine("Spawned FakeItemFrame#" + frame.getId() + " for Player#" + playerName);
|
||||
plugin.fine("Spawned FakeItemFrame#" + frame.getId() + " for Player#" + playerName);
|
||||
}
|
||||
|
||||
// Send packets
|
||||
@@ -473,7 +460,7 @@ public class FakeImage extends FakeEntity {
|
||||
* @param player Player instance or NULL for all observing players
|
||||
*/
|
||||
public void destroy(@Nullable Player player) {
|
||||
LOGGER.fine(
|
||||
plugin.fine(
|
||||
"Received request to destroy FakeImage#(" + location + "," + face + ") for " +
|
||||
(player == null ? "all players" : "Player#" + player.getName())
|
||||
);
|
||||
@@ -486,7 +473,7 @@ public class FakeImage extends FakeEntity {
|
||||
List<PacketContainer> packets = new ArrayList<>();
|
||||
for (FakeItemFrame frame : frames) {
|
||||
packets.add(frame.getDestroyPacket());
|
||||
LOGGER.fine("Destroyed FakeItemFrame#" + frame.getId() + " for Player#" + targetName);
|
||||
plugin.fine("Destroyed FakeItemFrame#" + frame.getId() + " for Player#" + targetName);
|
||||
}
|
||||
tryToSendPackets(target, packets);
|
||||
}
|
||||
@@ -527,13 +514,12 @@ public class FakeImage extends FakeEntity {
|
||||
task.cancel(true);
|
||||
task = null;
|
||||
currentStep = -1;
|
||||
LOGGER.fine("Destroyed animation task for FakeImage#(" + location + "," + face + ")");
|
||||
plugin.fine("Destroyed animation task for FakeImage#(" + location + "," + face + ")");
|
||||
}
|
||||
|
||||
// Free array of fake item frames
|
||||
frames = null;
|
||||
loading = false;
|
||||
LOGGER.fine("Invalidated FakeImage#(" + location + "," + face + ")");
|
||||
plugin.fine("Invalidated FakeImage#(" + location + "," + face + ")");
|
||||
|
||||
// Notify invalidation to source ImageFile
|
||||
ImageFile file = getFile();
|
||||
|
||||
@@ -8,7 +8,6 @@ import io.josemmo.bukkit.plugin.packets.DestroyEntityPacket;
|
||||
import io.josemmo.bukkit.plugin.packets.EntityMetadataPacket;
|
||||
import io.josemmo.bukkit.plugin.packets.SpawnEntityPacket;
|
||||
import io.josemmo.bukkit.plugin.utils.Internals;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Rotation;
|
||||
@@ -25,8 +24,7 @@ public class FakeItemFrame extends FakeEntity {
|
||||
public static final int MIN_FRAME_ID = Integer.MAX_VALUE / 4;
|
||||
public static final int MAX_FRAME_ID = Integer.MAX_VALUE;
|
||||
private static final boolean SUPPORTS_GLOWING = Internals.MINECRAFT_VERSION >= 17;
|
||||
private static final Logger LOGGER = Logger.getLogger("FakeItemFrame");
|
||||
private static final AtomicInteger LAST_FRAME_ID = new AtomicInteger(MAX_FRAME_ID);
|
||||
private static final AtomicInteger lastFrameId = new AtomicInteger(MAX_FRAME_ID);
|
||||
private final int id;
|
||||
private final Location location;
|
||||
private final BlockFace face;
|
||||
@@ -39,8 +37,8 @@ public class FakeItemFrame extends FakeEntity {
|
||||
* @return Next unused item frame ID
|
||||
*/
|
||||
private static int getNextId() {
|
||||
return LAST_FRAME_ID.updateAndGet(lastId -> {
|
||||
if (lastId == MAX_FRAME_ID) {
|
||||
return lastFrameId.updateAndGet(lastId -> {
|
||||
if (lastId >= MAX_FRAME_ID) {
|
||||
return MIN_FRAME_ID;
|
||||
}
|
||||
return lastId + 1;
|
||||
@@ -68,7 +66,7 @@ public class FakeItemFrame extends FakeEntity {
|
||||
this.rotation = rotation;
|
||||
this.glowing = glowing;
|
||||
this.maps = maps;
|
||||
LOGGER.fine("Created FakeItemFrame#" + this.id + " using " + this.maps.length + " FakeMap(s)");
|
||||
plugin.fine("Created FakeItemFrame#" + this.id + " using " + this.maps.length + " FakeMap(s)");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package io.josemmo.bukkit.plugin.renderer;
|
||||
|
||||
import io.josemmo.bukkit.plugin.packets.MapDataPacket;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.map.MapPalette;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.awt.*;
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
@@ -16,12 +14,11 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class FakeMap extends FakeEntity {
|
||||
public static final int DIMENSION = 128;
|
||||
private static final int MIN_MAP_ID = Integer.MAX_VALUE / 4;
|
||||
private static final int MAX_MAP_ID = Integer.MAX_VALUE;
|
||||
private static final int RESEND_THRESHOLD = 60*5; // Seconds after sending pixels when resending should be avoided
|
||||
private static final Logger LOGGER = Logger.getLogger("FakeMap");
|
||||
private static final AtomicInteger LAST_MAP_ID = new AtomicInteger(MIN_MAP_ID);
|
||||
private static @Nullable FakeMap ERROR_INSTANCE;
|
||||
public static final int MIN_MAP_ID = Integer.MAX_VALUE / 4;
|
||||
public static final int MAX_MAP_ID = Integer.MAX_VALUE;
|
||||
public static final int RESEND_THRESHOLD = 60*5; // Seconds after sending pixels when resending should be avoided
|
||||
private static final AtomicInteger lastMapId = new AtomicInteger(-1);
|
||||
private static FakeMap errorInstance;
|
||||
private final int id;
|
||||
private final byte[] pixels;
|
||||
private final ConcurrentMap<UUID, Long> lastPlayerSendTime = new ConcurrentHashMap<>();
|
||||
@@ -31,8 +28,8 @@ public class FakeMap extends FakeEntity {
|
||||
* @return Next unused map ID
|
||||
*/
|
||||
private static int getNextId() {
|
||||
return LAST_MAP_ID.updateAndGet(lastId -> {
|
||||
if (lastId == MIN_MAP_ID) {
|
||||
return lastMapId.updateAndGet(lastId -> {
|
||||
if (lastId <= MIN_MAP_ID) {
|
||||
return MAX_MAP_ID;
|
||||
}
|
||||
return lastId - 1;
|
||||
@@ -54,27 +51,27 @@ public class FakeMap extends FakeEntity {
|
||||
* @return Error instance
|
||||
*/
|
||||
private static @NotNull FakeMap getErrorInstance() {
|
||||
if (ERROR_INSTANCE == null) {
|
||||
if (errorInstance == null) {
|
||||
byte[] pixels = new byte[DIMENSION * DIMENSION];
|
||||
Arrays.fill(pixels, pixelToIndex(Color.RED.getRGB()));
|
||||
ERROR_INSTANCE = new FakeMap(pixels);
|
||||
errorInstance = new FakeMap(pixels);
|
||||
}
|
||||
return ERROR_INSTANCE;
|
||||
return errorInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get matrix of error maps
|
||||
* @param width Width in blocks
|
||||
* @param height Height in blocks
|
||||
* @return Fake maps
|
||||
* @return Fake maps container
|
||||
*/
|
||||
public static @NotNull FakeMap[][][] getErrorMatrix(int width, int height) {
|
||||
public static @NotNull FakeMapsContainer getErrorMatrix(int width, int height) {
|
||||
FakeMap[] errorMaps = new FakeMap[] {getErrorInstance()};
|
||||
FakeMap[][][] matrix = new FakeMap[width][height][1];
|
||||
for (FakeMap[][] column : matrix) {
|
||||
Arrays.fill(column, errorMaps);
|
||||
}
|
||||
return matrix;
|
||||
return new FakeMapsContainer(matrix, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,7 +90,7 @@ public class FakeMap extends FakeEntity {
|
||||
System.arraycopy(pixels, startX+(startY+y)*scanSize, this.pixels, y*DIMENSION, DIMENSION);
|
||||
}
|
||||
|
||||
LOGGER.fine("Created FakeMap#" + this.id);
|
||||
plugin.fine("Created FakeMap#" + this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +100,7 @@ public class FakeMap extends FakeEntity {
|
||||
public FakeMap(byte[] pixels) {
|
||||
this.id = getNextId();
|
||||
this.pixels = pixels;
|
||||
LOGGER.fine("Created FakeMap#" + this.id);
|
||||
plugin.fine("Created FakeMap#" + this.id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +136,7 @@ public class FakeMap extends FakeEntity {
|
||||
|
||||
// Authorize re-send and update latest timestamp
|
||||
lastPlayerSendTime.put(uuid, now);
|
||||
LOGGER.fine("Granted sending pixels for FakeMap#" + id + " to Player#" + player.getName());
|
||||
plugin.fine("Granted sending pixels for FakeMap#" + id + " to Player#" + player.getName());
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.josemmo.bukkit.plugin.renderer;
|
||||
|
||||
public class FakeMapsContainer {
|
||||
private final FakeMap[][][] fakeMaps;
|
||||
private final int delay;
|
||||
|
||||
public FakeMapsContainer(FakeMap[][][] fakeMaps, int delay) {
|
||||
this.fakeMaps = fakeMaps;
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get fake maps
|
||||
* @return Tri-dimensional array of maps (column, row, step)
|
||||
*/
|
||||
public FakeMap[][][] getFakeMaps() {
|
||||
return fakeMaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delay between steps
|
||||
* @return Delay in 50ms intervals or <code>0</code> if not applicable
|
||||
*/
|
||||
public int getDelay() {
|
||||
return delay;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package io.josemmo.bukkit.plugin.renderer;
|
||||
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import io.josemmo.bukkit.plugin.utils.CsvConfiguration;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.BlockFace;
|
||||
import org.bukkit.entity.Entity;
|
||||
@@ -18,19 +17,18 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ImageRenderer implements Listener {
|
||||
private static final long SAVE_INTERVAL = 20L * 90; // In server ticks
|
||||
private static final Logger LOGGER = Logger.getLogger("ImageRenderer");
|
||||
private final Path configPath;
|
||||
private final boolean animateImages;
|
||||
private final int maxImageDimension;
|
||||
public static final long SAVE_INTERVAL = 20L * 90; // In server ticks
|
||||
private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private final String configPath;
|
||||
private BukkitTask saveTask;
|
||||
private final AtomicBoolean hasConfigChanged = new AtomicBoolean(false);
|
||||
private final ConcurrentMap<WorldAreaId, Set<FakeImage>> images = new ConcurrentHashMap<>();
|
||||
@@ -39,30 +37,10 @@ public class ImageRenderer implements Listener {
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param configPath Path to configuration file
|
||||
* @param animateImages Whether to animate images or not
|
||||
* @param maxImageDimension Maximum image dimension in blocks
|
||||
* @param configPath Path to configuration file
|
||||
*/
|
||||
public ImageRenderer(@NotNull Path configPath, boolean animateImages, int maxImageDimension) {
|
||||
public ImageRenderer(@NotNull String configPath) {
|
||||
this.configPath = configPath;
|
||||
this.animateImages = animateImages;
|
||||
this.maxImageDimension = maxImageDimension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is animation enabled
|
||||
* @return Is animation enabled
|
||||
*/
|
||||
public boolean isAnimationEnabled() {
|
||||
return animateImages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum image dimension
|
||||
* @return Maximum image dimension in blocks
|
||||
*/
|
||||
public int getMaxImageDimension() {
|
||||
return maxImageDimension;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,7 +48,6 @@ public class ImageRenderer implements Listener {
|
||||
*/
|
||||
public void start() {
|
||||
loadConfig();
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
saveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::saveConfig, SAVE_INTERVAL, SAVE_INTERVAL);
|
||||
}
|
||||
@@ -104,8 +81,8 @@ public class ImageRenderer implements Listener {
|
||||
* Load configuration from disk
|
||||
*/
|
||||
private void loadConfig() {
|
||||
if (!Files.isRegularFile(configPath)) {
|
||||
LOGGER.info("No placed fake images configuration file found");
|
||||
if (!Files.isRegularFile(Paths.get(configPath))) {
|
||||
plugin.info("No placed fake images configuration file found");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -114,7 +91,7 @@ public class ImageRenderer implements Listener {
|
||||
try {
|
||||
config.load(configPath);
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Failed to load placed fake images from disk", e);
|
||||
plugin.log(Level.SEVERE, "Failed to load placed fake images from disk", e);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -122,19 +99,19 @@ public class ImageRenderer implements Listener {
|
||||
for (String[] row : config.getRows()) {
|
||||
try {
|
||||
String filename = row[0];
|
||||
World world = Objects.requireNonNull(YamipaPlugin.getInstance().getServer().getWorld(row[1]));
|
||||
World world = Objects.requireNonNull(plugin.getServer().getWorld(row[1]));
|
||||
double x = Integer.parseInt(row[2]);
|
||||
double y = Integer.parseInt(row[3]);
|
||||
double z = Integer.parseInt(row[4]);
|
||||
Location location = new Location(world, x, y, z);
|
||||
BlockFace face = BlockFace.valueOf(row[5]);
|
||||
Rotation rotation = Rotation.valueOf(row[6]);
|
||||
int width = Math.abs(Integer.parseInt(row[7]));
|
||||
int height = Math.abs(Integer.parseInt(row[8]));
|
||||
Date placedAt = (row.length > 9 && !row[9].isEmpty()) ?
|
||||
int width = Math.min(FakeImage.MAX_DIMENSION, Math.abs(Integer.parseInt(row[7])));
|
||||
int height = Math.min(FakeImage.MAX_DIMENSION, Math.abs(Integer.parseInt(row[8])));
|
||||
Date placedAt = (row.length > 9 && !row[9].equals("")) ?
|
||||
new Date(Long.parseLong(row[9])*1000L) :
|
||||
null;
|
||||
UUID placedById = (row.length > 10 && !row[10].isEmpty()) ?
|
||||
UUID placedById = (row.length > 10 && !row[10].equals("")) ?
|
||||
UUID.fromString(row[10]) :
|
||||
FakeImage.UNKNOWN_PLAYER_ID;
|
||||
OfflinePlayer placedBy = Bukkit.getOfflinePlayer(placedById);
|
||||
@@ -145,7 +122,7 @@ public class ImageRenderer implements Listener {
|
||||
placedAt, placedBy, flags);
|
||||
addImage(fakeImage, true);
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Invalid fake image properties: " + String.join(";", row), e);
|
||||
plugin.log(Level.SEVERE, "Invalid fake image properties: " + String.join(";", row), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,7 +130,6 @@ public class ImageRenderer implements Listener {
|
||||
/**
|
||||
* Save configuration to disk
|
||||
*/
|
||||
@SuppressWarnings("ExtractMethodRecommender")
|
||||
private void saveConfig() {
|
||||
if (!hasConfigChanged.get()) return;
|
||||
|
||||
@@ -191,9 +167,9 @@ public class ImageRenderer implements Listener {
|
||||
// Write to disk
|
||||
try {
|
||||
config.save(configPath);
|
||||
LOGGER.info("Saved placed fake images to disk");
|
||||
plugin.info("Saved placed fake images to disk");
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Failed to save placed fake images to disk", e);
|
||||
plugin.log(Level.SEVERE, "Failed to save placed fake images to disk", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,7 +184,7 @@ public class ImageRenderer implements Listener {
|
||||
// Add image to renderer
|
||||
for (WorldAreaId worldAreaId : imageWorldAreaIds) {
|
||||
images.computeIfAbsent(worldAreaId, __ -> {
|
||||
LOGGER.fine("Created WorldArea#(" + worldAreaId + ")");
|
||||
plugin.fine("Created WorldArea#(" + worldAreaId + ")");
|
||||
return ConcurrentHashMap.newKeySet();
|
||||
}).add(image);
|
||||
}
|
||||
@@ -267,7 +243,7 @@ public class ImageRenderer implements Listener {
|
||||
* @param maxX Maximum X coordinate
|
||||
* @param minZ Minimum Z coordinate
|
||||
* @param maxZ Maximum Z coordinate
|
||||
* @return Set of found images
|
||||
* @return List of found images
|
||||
*/
|
||||
public @NotNull Set<FakeImage> getImages(@NotNull World world, int minX, int maxX, int minZ, int maxZ) {
|
||||
Set<FakeImage> response = new HashSet<>();
|
||||
@@ -298,7 +274,7 @@ public class ImageRenderer implements Listener {
|
||||
Set<FakeImage> worldAreaImages = images.get(worldAreaId);
|
||||
worldAreaImages.remove(image);
|
||||
if (worldAreaImages.isEmpty()) {
|
||||
LOGGER.fine("Destroyed WorldArea#(" + worldAreaId + ")");
|
||||
plugin.fine("Destroyed WorldArea#(" + worldAreaId + ")");
|
||||
images.remove(worldAreaId);
|
||||
}
|
||||
}
|
||||
@@ -390,7 +366,7 @@ public class ImageRenderer implements Listener {
|
||||
private void onPlayerLocationChange(@NotNull Player player, @NotNull Location location) {
|
||||
// Ignore NPC events from other plugins
|
||||
if (player.hasMetadata("NPC")) {
|
||||
LOGGER.fine("Ignored NPC event from Player#" + player.getName());
|
||||
plugin.fine("Ignored NPC event from Player#" + player.getName());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -401,7 +377,7 @@ public class ImageRenderer implements Listener {
|
||||
return;
|
||||
}
|
||||
playersLocation.put(player, worldAreaId);
|
||||
LOGGER.fine("Player#" + player.getName() + " moved to WorldArea#(" + worldAreaId + ")");
|
||||
plugin.fine("Player#" + player.getName() + " moved to WorldArea#(" + worldAreaId + ")");
|
||||
|
||||
// Get images that should be spawned/destroyed
|
||||
Set<FakeImage> desiredState = getImagesInViewDistance(worldAreaId);
|
||||
@@ -452,8 +428,9 @@ public class ImageRenderer implements Listener {
|
||||
|
||||
// Wait until next server tick before handling location change
|
||||
// This is necessary as teleport events get fired *before* teleporting the player
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
Bukkit.getScheduler().runTask(plugin, () -> onPlayerLocationChange(event.getPlayer(), event.getTo()));
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
onPlayerLocationChange(event.getPlayer(), event.getTo());
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
|
||||
|
||||
@@ -5,7 +5,6 @@ import io.josemmo.bukkit.plugin.commands.ImageCommand;
|
||||
import io.josemmo.bukkit.plugin.storage.ImageFile;
|
||||
import io.josemmo.bukkit.plugin.utils.ActionBar;
|
||||
import io.josemmo.bukkit.plugin.utils.InteractWithEntityListener;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.block.Block;
|
||||
import org.bukkit.block.BlockFace;
|
||||
@@ -28,19 +27,11 @@ import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ItemService extends InteractWithEntityListener implements Listener {
|
||||
private static final Logger LOGGER = Logger.getLogger("ItemService");
|
||||
private static final NamespacedKey NSK_FILENAME;
|
||||
private static final NamespacedKey NSK_WIDTH;
|
||||
private static final NamespacedKey NSK_HEIGHT;
|
||||
private static final NamespacedKey NSK_FLAGS;
|
||||
|
||||
static {
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance(); // Only used for getting namespace, reference will be freed
|
||||
NSK_FILENAME = new NamespacedKey(plugin, "filename");
|
||||
NSK_WIDTH = new NamespacedKey(plugin, "width");
|
||||
NSK_HEIGHT = new NamespacedKey(plugin, "height");
|
||||
NSK_FLAGS = new NamespacedKey(plugin, "flags");
|
||||
}
|
||||
private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private static final NamespacedKey NSK_FILENAME = new NamespacedKey(plugin, "filename");
|
||||
private static final NamespacedKey NSK_WIDTH = new NamespacedKey(plugin, "width");
|
||||
private static final NamespacedKey NSK_HEIGHT = new NamespacedKey(plugin, "height");
|
||||
private static final NamespacedKey NSK_FLAGS = new NamespacedKey(plugin, "flags");
|
||||
|
||||
/**
|
||||
* Get image item
|
||||
@@ -57,9 +48,9 @@ public class ItemService extends InteractWithEntityListener implements Listener
|
||||
|
||||
// Set metadata
|
||||
PersistentDataContainer itemData = itemMeta.getPersistentDataContainer();
|
||||
itemMeta.setDisplayName(image.getFilename() + ChatColor.AQUA + " (" + width + "x" + height + ")");
|
||||
itemMeta.setDisplayName(image.getName() + ChatColor.AQUA + " (" + width + "x" + height + ")");
|
||||
itemMeta.setLore(Collections.singletonList("Yamipa image"));
|
||||
itemData.set(NSK_FILENAME, PersistentDataType.STRING, image.getFilename());
|
||||
itemData.set(NSK_FILENAME, PersistentDataType.STRING, image.getName());
|
||||
itemData.set(NSK_WIDTH, PersistentDataType.INTEGER, width);
|
||||
itemData.set(NSK_HEIGHT, PersistentDataType.INTEGER, height);
|
||||
itemData.set(NSK_FLAGS, PersistentDataType.INTEGER, flags);
|
||||
@@ -73,7 +64,6 @@ public class ItemService extends InteractWithEntityListener implements Listener
|
||||
*/
|
||||
public void start() {
|
||||
register();
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
@@ -142,14 +132,14 @@ public class ItemService extends InteractWithEntityListener implements Listener
|
||||
return;
|
||||
}
|
||||
if (width == null || height == null || flags == null) {
|
||||
LOGGER.warning(player + " tried to place corrupted image item (missing width/height/flags properties)");
|
||||
plugin.warning(player + " tried to place corrupted image item (missing width/height/flags properties)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate filename
|
||||
ImageFile image = YamipaPlugin.getInstance().getStorage().get(filename);
|
||||
if (image == null) {
|
||||
LOGGER.warning(player + " tried to place corrupted image item (\"" + filename + "\" no longer exists)");
|
||||
plugin.warning(player + " tried to place corrupted image item (\"" + filename + "\" no longer exists)");
|
||||
ActionBar.send(player, ChatColor.RED + "Image file \"" + filename + "\" no longer exists");
|
||||
return;
|
||||
}
|
||||
@@ -186,7 +176,7 @@ public class ItemService extends InteractWithEntityListener implements Listener
|
||||
|
||||
@Override
|
||||
public boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
|
||||
ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
|
||||
ImageRenderer renderer = plugin.getRenderer();
|
||||
Location location = block.getLocation();
|
||||
|
||||
// Has the player clicked a removable placed image?
|
||||
@@ -217,8 +207,9 @@ public class ItemService extends InteractWithEntityListener implements Listener
|
||||
ImageFile imageFile = Objects.requireNonNull(image.getFile());
|
||||
ItemStack imageItem = getImageItem(imageFile, 1, image.getWidth(), image.getHeight(), image.getFlags());
|
||||
Location dropLocation = location.clone().add(0.5, -0.5, 0.5).add(face.getDirection());
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> block.getWorld().dropItem(dropLocation, imageItem));
|
||||
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
|
||||
block.getWorld().dropItem(dropLocation, imageItem);
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -61,7 +61,7 @@ public class WorldAreaId {
|
||||
|
||||
/**
|
||||
* Get nearby world area IDs in view distance (plus this one)
|
||||
* @return Array of neighbors
|
||||
* @return List of neighbors
|
||||
*/
|
||||
public @NotNull WorldAreaId[] getNeighborhood() {
|
||||
// Get value from cache (if available)
|
||||
@@ -76,127 +76,16 @@ public class WorldAreaId {
|
||||
return distance / 4;
|
||||
});
|
||||
|
||||
// Size 0 (1+1+1=3)
|
||||
// ·|·
|
||||
// XOX
|
||||
// ·|·
|
||||
if (size == 0) {
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x, z-1),
|
||||
|
||||
new WorldAreaId(world, x-1, z),
|
||||
this,
|
||||
new WorldAreaId(world, x+1, z),
|
||||
|
||||
new WorldAreaId(world, x, z+1),
|
||||
};
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
// Size 1 (2+1+2=5)
|
||||
// ·X|X·
|
||||
// XX|XX
|
||||
// XXOXX
|
||||
// XX|XX
|
||||
// ·X|X·
|
||||
if (size == 1) {
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x-1, z-2),
|
||||
new WorldAreaId(world, x, z-2),
|
||||
new WorldAreaId(world, x+1, z-2),
|
||||
|
||||
new WorldAreaId(world, x-2, z-1),
|
||||
new WorldAreaId(world, x-1, z-1),
|
||||
new WorldAreaId(world, x, z-1),
|
||||
new WorldAreaId(world, x+1, z-1),
|
||||
new WorldAreaId(world, x+2, z-1),
|
||||
|
||||
new WorldAreaId(world, x-2, z),
|
||||
new WorldAreaId(world, x-1, z),
|
||||
this,
|
||||
new WorldAreaId(world, x+1, z),
|
||||
new WorldAreaId(world, x+2, z),
|
||||
|
||||
new WorldAreaId(world, x-2, z+1),
|
||||
new WorldAreaId(world, x-1, z+1),
|
||||
new WorldAreaId(world, x, z+1),
|
||||
new WorldAreaId(world, x+1, z+1),
|
||||
new WorldAreaId(world, x+2, z+1),
|
||||
|
||||
new WorldAreaId(world, x-1, z+2),
|
||||
new WorldAreaId(world, x, z+2),
|
||||
new WorldAreaId(world, x+1, z+2),
|
||||
};
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
// Size 2 (3+1+3=7)
|
||||
// ··X|X··
|
||||
// ·XX|XX·
|
||||
// XXX|XXX
|
||||
// XXXOXXX
|
||||
// XXX|XXX
|
||||
// ·XX|XX·
|
||||
// ··X|X··
|
||||
if (size == 2) {
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x-1, z-3),
|
||||
new WorldAreaId(world, x, z-3),
|
||||
new WorldAreaId(world, x+1, z-3),
|
||||
|
||||
new WorldAreaId(world, x-2, z-2),
|
||||
new WorldAreaId(world, x-1, z-2),
|
||||
new WorldAreaId(world, x, z-2),
|
||||
new WorldAreaId(world, x+1, z-2),
|
||||
new WorldAreaId(world, x+2, z-2),
|
||||
|
||||
new WorldAreaId(world, x-3, z-1),
|
||||
new WorldAreaId(world, x-2, z-1),
|
||||
new WorldAreaId(world, x-1, z-1),
|
||||
new WorldAreaId(world, x, z-1),
|
||||
new WorldAreaId(world, x+1, z-1),
|
||||
new WorldAreaId(world, x+2, z-1),
|
||||
new WorldAreaId(world, x+3, z-1),
|
||||
|
||||
new WorldAreaId(world, x-3, z),
|
||||
new WorldAreaId(world, x-2, z),
|
||||
new WorldAreaId(world, x-1, z),
|
||||
this,
|
||||
new WorldAreaId(world, x+1, z),
|
||||
new WorldAreaId(world, x+2, z),
|
||||
new WorldAreaId(world, x+3, z),
|
||||
|
||||
new WorldAreaId(world, x-3, z+1),
|
||||
new WorldAreaId(world, x-2, z+1),
|
||||
new WorldAreaId(world, x-1, z+1),
|
||||
new WorldAreaId(world, x, z+1),
|
||||
new WorldAreaId(world, x+1, z+1),
|
||||
new WorldAreaId(world, x+2, z+1),
|
||||
new WorldAreaId(world, x+3, z+1),
|
||||
|
||||
new WorldAreaId(world, x-2, z+2),
|
||||
new WorldAreaId(world, x-1, z+2),
|
||||
new WorldAreaId(world, x, z+2),
|
||||
new WorldAreaId(world, x+1, z+2),
|
||||
new WorldAreaId(world, x+2, z+2),
|
||||
|
||||
new WorldAreaId(world, x-1, z+3),
|
||||
new WorldAreaId(world, x, z+3),
|
||||
new WorldAreaId(world, x+1, z+3),
|
||||
};
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
// Size 3 (4+1+4=9)
|
||||
// ···X|X···
|
||||
// ·XXX|XXX·
|
||||
// ·XXX|XXX·
|
||||
// XXXX|XXXX
|
||||
// Size 3 (16-chunks radius)
|
||||
// ···XXX···
|
||||
// ·XXXXXXX·
|
||||
// ·XXXXXXX·
|
||||
// XXXXXXXXX
|
||||
// XXXXOXXXX
|
||||
// XXXX|XXXX
|
||||
// ·XXX|XXX·
|
||||
// ·XXX|XXX·
|
||||
// ···X|X···
|
||||
// XXXXXXXXX
|
||||
// ·XXXXXXX·
|
||||
// ·XXXXXXX·
|
||||
// ···XXX···
|
||||
if (size == 3) {
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x-1, z-4),
|
||||
@@ -272,126 +161,112 @@ public class WorldAreaId {
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
// Size ≥4 (5+1+5=11)
|
||||
// ···XX|XX···
|
||||
// ··XXX|XXX··
|
||||
// ·XXXX|XXXX·
|
||||
// XXXXX|XXXXX
|
||||
// XXXXX|XXXXX
|
||||
// XXXXXOXXXXX
|
||||
// XXXXX|XXXXX
|
||||
// XXXXX|XXXXX
|
||||
// ·XXXX|XXXX·
|
||||
// ··XXX|XXX··
|
||||
// ···XX|XX···
|
||||
// Size 2 (12-chunks radius)
|
||||
// ··XXX··
|
||||
// ·XXXXX·
|
||||
// XXXXXXX
|
||||
// XXXOXXX
|
||||
// XXXXXXX
|
||||
// ·XXXXX·
|
||||
// ··XXX··
|
||||
if (size == 2) {
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x-1, z-3),
|
||||
new WorldAreaId(world, x, z-3),
|
||||
new WorldAreaId(world, x+1, z-3),
|
||||
|
||||
new WorldAreaId(world, x-2, z-2),
|
||||
new WorldAreaId(world, x-1, z-2),
|
||||
new WorldAreaId(world, x, z-2),
|
||||
new WorldAreaId(world, x+1, z-2),
|
||||
new WorldAreaId(world, x+2, z-2),
|
||||
|
||||
new WorldAreaId(world, x-3, z-1),
|
||||
new WorldAreaId(world, x-2, z-1),
|
||||
new WorldAreaId(world, x-1, z-1),
|
||||
new WorldAreaId(world, x, z-1),
|
||||
new WorldAreaId(world, x+1, z-1),
|
||||
new WorldAreaId(world, x+2, z-1),
|
||||
new WorldAreaId(world, x+3, z-1),
|
||||
|
||||
new WorldAreaId(world, x-3, z),
|
||||
new WorldAreaId(world, x-2, z),
|
||||
new WorldAreaId(world, x-1, z),
|
||||
this,
|
||||
new WorldAreaId(world, x+1, z),
|
||||
new WorldAreaId(world, x+2, z),
|
||||
new WorldAreaId(world, x+3, z),
|
||||
|
||||
new WorldAreaId(world, x-3, z+1),
|
||||
new WorldAreaId(world, x-2, z+1),
|
||||
new WorldAreaId(world, x-1, z+1),
|
||||
new WorldAreaId(world, x, z+1),
|
||||
new WorldAreaId(world, x+1, z+1),
|
||||
new WorldAreaId(world, x+2, z+1),
|
||||
new WorldAreaId(world, x+3, z+1),
|
||||
|
||||
new WorldAreaId(world, x-2, z+2),
|
||||
new WorldAreaId(world, x-1, z+2),
|
||||
new WorldAreaId(world, x, z+2),
|
||||
new WorldAreaId(world, x+1, z+2),
|
||||
new WorldAreaId(world, x+2, z+2),
|
||||
|
||||
new WorldAreaId(world, x-1, z+3),
|
||||
new WorldAreaId(world, x, z+3),
|
||||
new WorldAreaId(world, x+1, z+3),
|
||||
};
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
//// Size 1 (8-chunks radius)
|
||||
// ·XXX·
|
||||
// XXXXX
|
||||
// XXOXX
|
||||
// XXXXX
|
||||
// ·XXX·
|
||||
if (size == 1) {
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x-1, z-2),
|
||||
new WorldAreaId(world, x, z-2),
|
||||
new WorldAreaId(world, x+1, z-2),
|
||||
|
||||
new WorldAreaId(world, x-2, z-1),
|
||||
new WorldAreaId(world, x-1, z-1),
|
||||
new WorldAreaId(world, x, z-1),
|
||||
new WorldAreaId(world, x+1, z-1),
|
||||
new WorldAreaId(world, x+2, z-1),
|
||||
|
||||
new WorldAreaId(world, x-2, z),
|
||||
new WorldAreaId(world, x-1, z),
|
||||
this,
|
||||
new WorldAreaId(world, x+1, z),
|
||||
new WorldAreaId(world, x+2, z),
|
||||
|
||||
new WorldAreaId(world, x-2, z+1),
|
||||
new WorldAreaId(world, x-1, z+1),
|
||||
new WorldAreaId(world, x, z+1),
|
||||
new WorldAreaId(world, x+1, z+1),
|
||||
new WorldAreaId(world, x+2, z+1),
|
||||
|
||||
new WorldAreaId(world, x-1, z+2),
|
||||
new WorldAreaId(world, x, z+2),
|
||||
new WorldAreaId(world, x+1, z+2),
|
||||
};
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
//// Size 0 (4x4 chunks)
|
||||
// ·X·
|
||||
// XOX
|
||||
// ·X·
|
||||
neighborhood = new WorldAreaId[]{
|
||||
new WorldAreaId(world, x-2, z-5),
|
||||
new WorldAreaId(world, x-1, z-5),
|
||||
new WorldAreaId(world, x, z-5),
|
||||
new WorldAreaId(world, x+1, z-5),
|
||||
new WorldAreaId(world, x+2, z-5),
|
||||
|
||||
new WorldAreaId(world, x-3, z-4),
|
||||
new WorldAreaId(world, x-2, z-4),
|
||||
new WorldAreaId(world, x-1, z-4),
|
||||
new WorldAreaId(world, x, z-4),
|
||||
new WorldAreaId(world, x+1, z-4),
|
||||
new WorldAreaId(world, x+2, z-4),
|
||||
new WorldAreaId(world, x+3, z-4),
|
||||
|
||||
new WorldAreaId(world, x-4, z-3),
|
||||
new WorldAreaId(world, x-3, z-3),
|
||||
new WorldAreaId(world, x-2, z-3),
|
||||
new WorldAreaId(world, x-1, z-3),
|
||||
new WorldAreaId(world, x, z-3),
|
||||
new WorldAreaId(world, x+1, z-3),
|
||||
new WorldAreaId(world, x+2, z-3),
|
||||
new WorldAreaId(world, x+3, z-3),
|
||||
new WorldAreaId(world, x+4, z-3),
|
||||
|
||||
new WorldAreaId(world, x-5, z-2),
|
||||
new WorldAreaId(world, x-4, z-2),
|
||||
new WorldAreaId(world, x-3, z-2),
|
||||
new WorldAreaId(world, x-2, z-2),
|
||||
new WorldAreaId(world, x-1, z-2),
|
||||
new WorldAreaId(world, x, z-2),
|
||||
new WorldAreaId(world, x+1, z-2),
|
||||
new WorldAreaId(world, x+2, z-2),
|
||||
new WorldAreaId(world, x+3, z-2),
|
||||
new WorldAreaId(world, x+4, z-2),
|
||||
new WorldAreaId(world, x+5, z-2),
|
||||
|
||||
new WorldAreaId(world, x-5, z-1),
|
||||
new WorldAreaId(world, x-4, z-1),
|
||||
new WorldAreaId(world, x-3, z-1),
|
||||
new WorldAreaId(world, x-2, z-1),
|
||||
new WorldAreaId(world, x-1, z-1),
|
||||
new WorldAreaId(world, x, z-1),
|
||||
new WorldAreaId(world, x+1, z-1),
|
||||
new WorldAreaId(world, x+2, z-1),
|
||||
new WorldAreaId(world, x+3, z-1),
|
||||
new WorldAreaId(world, x+4, z-1),
|
||||
new WorldAreaId(world, x+5, z-1),
|
||||
|
||||
new WorldAreaId(world, x-5, z),
|
||||
new WorldAreaId(world, x-4, z),
|
||||
new WorldAreaId(world, x-3, z),
|
||||
new WorldAreaId(world, x-2, z),
|
||||
new WorldAreaId(world, x-1, z),
|
||||
this,
|
||||
new WorldAreaId(world, x+1, z),
|
||||
new WorldAreaId(world, x+2, z),
|
||||
new WorldAreaId(world, x+3, z),
|
||||
new WorldAreaId(world, x+4, z),
|
||||
new WorldAreaId(world, x+5, z),
|
||||
|
||||
new WorldAreaId(world, x-5, z+1),
|
||||
new WorldAreaId(world, x-4, z+1),
|
||||
new WorldAreaId(world, x-3, z+1),
|
||||
new WorldAreaId(world, x-2, z+1),
|
||||
new WorldAreaId(world, x-1, z+1),
|
||||
new WorldAreaId(world, x, z+1),
|
||||
new WorldAreaId(world, x+1, z+1),
|
||||
new WorldAreaId(world, x+2, z+1),
|
||||
new WorldAreaId(world, x+3, z+1),
|
||||
new WorldAreaId(world, x+4, z+1),
|
||||
new WorldAreaId(world, x+5, z+1),
|
||||
|
||||
new WorldAreaId(world, x-5, z+2),
|
||||
new WorldAreaId(world, x-4, z+2),
|
||||
new WorldAreaId(world, x-3, z+2),
|
||||
new WorldAreaId(world, x-2, z+2),
|
||||
new WorldAreaId(world, x-1, z+2),
|
||||
new WorldAreaId(world, x, z+2),
|
||||
new WorldAreaId(world, x+1, z+2),
|
||||
new WorldAreaId(world, x+2, z+2),
|
||||
new WorldAreaId(world, x+3, z+2),
|
||||
new WorldAreaId(world, x+4, z+2),
|
||||
new WorldAreaId(world, x+5, z+2),
|
||||
|
||||
new WorldAreaId(world, x-4, z+3),
|
||||
new WorldAreaId(world, x-3, z+3),
|
||||
new WorldAreaId(world, x-2, z+3),
|
||||
new WorldAreaId(world, x-1, z+3),
|
||||
new WorldAreaId(world, x, z+3),
|
||||
new WorldAreaId(world, x+1, z+3),
|
||||
new WorldAreaId(world, x+2, z+3),
|
||||
new WorldAreaId(world, x+3, z+3),
|
||||
new WorldAreaId(world, x+4, z+3),
|
||||
|
||||
new WorldAreaId(world, x-3, z+4),
|
||||
new WorldAreaId(world, x-2, z+4),
|
||||
new WorldAreaId(world, x-1, z+4),
|
||||
new WorldAreaId(world, x, z+4),
|
||||
new WorldAreaId(world, x+1, z+4),
|
||||
new WorldAreaId(world, x+2, z+4),
|
||||
new WorldAreaId(world, x+3, z+4),
|
||||
|
||||
new WorldAreaId(world, x-2, z+5),
|
||||
new WorldAreaId(world, x-1, z+5),
|
||||
new WorldAreaId(world, x, z+5),
|
||||
new WorldAreaId(world, x+1, z+5),
|
||||
new WorldAreaId(world, x+2, z+5),
|
||||
};
|
||||
return neighborhood;
|
||||
}
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
package io.josemmo.bukkit.plugin.storage;
|
||||
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeImage;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeMap;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class CachedMapsFile extends SynchronizedFile {
|
||||
private static final String CACHE_EXT = "cache";
|
||||
private static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP"
|
||||
private static final int CACHE_VERSION = 1;
|
||||
private static final Logger LOGGER = Logger.getLogger("CachedMapsFile");
|
||||
private final ImageFile imageFile;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private FakeMap[][][] maps;
|
||||
private int delay;
|
||||
|
||||
/**
|
||||
* Create instance from image file
|
||||
* @param imageFile Image file instance
|
||||
* @param width Width in blocks
|
||||
* @param height Height in blocks
|
||||
* @return Cached maps instance
|
||||
*/
|
||||
public static @NotNull CachedMapsFile from(@NotNull ImageFile imageFile, int width, int height) {
|
||||
Path cachePath = YamipaPlugin.getInstance().getStorage().getCachePath();
|
||||
Path path = cachePath.resolve(imageFile.getFilename() + "." + width + "-" + height + "." + CACHE_EXT);
|
||||
return new CachedMapsFile(path, imageFile, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all cached maps files associated to an image file
|
||||
* @param imageFile Image file instance
|
||||
*/
|
||||
public static void deleteAll(@NotNull ImageFile imageFile) {
|
||||
String relativeFilename = imageFile.getFilename();
|
||||
Path cachePath = YamipaPlugin.getInstance().getStorage().getCachePath();
|
||||
File baseDirectory = cachePath.resolve(relativeFilename).getParent().toFile();
|
||||
String cachePattern = Pattern.quote(Paths.get(relativeFilename).getFileName().toString()) +
|
||||
"\\.[0-9]+-[0-9]+\\." + CACHE_EXT;
|
||||
|
||||
// Find cache files to delete
|
||||
if (!baseDirectory.exists()) {
|
||||
// Cache subdirectory does not exist, no need to delete files
|
||||
return;
|
||||
}
|
||||
File[] files = baseDirectory.listFiles((__, item) -> item.matches(cachePattern));
|
||||
if (files == null) {
|
||||
LOGGER.warning("An error occurred when listing cache files for image \"" + relativeFilename + "\"");
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete disk cache files
|
||||
for (File file : files) {
|
||||
if (!file.delete()) {
|
||||
LOGGER.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param path Path to cached maps file in disk
|
||||
* @param imageFile Image file associated to these maps
|
||||
* @param width Width in blocks
|
||||
* @param height Height blocks
|
||||
*/
|
||||
private CachedMapsFile(@NotNull Path path, @NotNull ImageFile imageFile, int width, int height) {
|
||||
super(path);
|
||||
this.imageFile = imageFile;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
load();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maps
|
||||
* @return Tri-dimensional array of maps (column, row, step)
|
||||
*/
|
||||
public @NotNull FakeMap[][][] getMaps() {
|
||||
return maps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get delay between steps
|
||||
* @return Delay in 50ms intervals or <code>0</code> if not applicable
|
||||
*/
|
||||
public int getDelay() {
|
||||
return delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load maps
|
||||
*/
|
||||
private void load() {
|
||||
// Try to load maps from disk
|
||||
if (exists() && getLastModified() > imageFile.getLastModified()) {
|
||||
LOGGER.fine("Found warm cache file \"" + path + "\"");
|
||||
try {
|
||||
loadFromDisk();
|
||||
return;
|
||||
} catch (IllegalArgumentException e) {
|
||||
LOGGER.info("Cache file \"" + path + "\" is outdated and will be overwritten");
|
||||
} catch (Exception e) {
|
||||
LOGGER.warning("Cache file \"" + path + "\" is corrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate maps from image file
|
||||
LOGGER.fine("Missed cache file \"" + path + "\"");
|
||||
try {
|
||||
generateFromImage();
|
||||
tryToWriteToDisk();
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to render image step(s) from file \"" + path + "\"", e);
|
||||
}
|
||||
|
||||
// Fallback to error matrix
|
||||
maps = FakeMap.getErrorMatrix(width, height);
|
||||
delay = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load data from disk
|
||||
* @throws IllegalArgumentException if cache file is outdated
|
||||
* @throws IOException if cache file is corrupted
|
||||
*/
|
||||
private void loadFromDisk() throws IllegalArgumentException, IOException {
|
||||
try (RandomAccessFile stream = read()) {
|
||||
// Validate file signature
|
||||
for (byte expectedByte : CACHE_SIGNATURE) {
|
||||
if ((byte) stream.read() != expectedByte) {
|
||||
throw new IllegalArgumentException("Invalid file signature");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate version number
|
||||
if ((byte) stream.read() != CACHE_VERSION) {
|
||||
throw new IllegalArgumentException("Incompatible file format version");
|
||||
}
|
||||
|
||||
// Get number of animation steps
|
||||
int numOfSteps = stream.read() | (stream.read() << 8);
|
||||
if (numOfSteps < 1 || numOfSteps > FakeImage.MAX_STEPS) {
|
||||
throw new IOException("Invalid number of animation steps: " + numOfSteps);
|
||||
}
|
||||
|
||||
// Get delay between steps
|
||||
int delay = 0;
|
||||
if (numOfSteps > 1) {
|
||||
delay = stream.read();
|
||||
if (delay < FakeImage.MIN_DELAY || delay > FakeImage.MAX_DELAY) {
|
||||
throw new IOException("Invalid delay between steps: " + delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Read pixels
|
||||
FakeMap[][][] maps = new FakeMap[width][height][numOfSteps];
|
||||
for (int col=0; col<width; ++col) {
|
||||
for (int row=0; row<height; ++row) {
|
||||
for (int step=0; step<numOfSteps; ++step) {
|
||||
byte[] buffer = new byte[FakeMap.DIMENSION*FakeMap.DIMENSION];
|
||||
stream.read(buffer);
|
||||
maps[col][row][step] = new FakeMap(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update instance state
|
||||
this.maps = maps;
|
||||
this.delay = delay;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate data from image
|
||||
* @throws IOException if an I/O error occurred
|
||||
* @throws RuntimeException if failed to render image steps
|
||||
*/
|
||||
private void generateFromImage() throws IOException, RuntimeException {
|
||||
int widthInPixels = width * FakeMap.DIMENSION;
|
||||
int heightInPixels = height * FakeMap.DIMENSION;
|
||||
|
||||
// Render image steps in Minecraft color palette
|
||||
List<byte[]> renderedImages = new ArrayList<>();
|
||||
Map<Integer, Integer> delays = new HashMap<>();
|
||||
try (ImageInputStream inputStream = ImageIO.createImageInputStream(imageFile.read())) {
|
||||
ImageReader reader = ImageIO.getImageReaders(inputStream).next();
|
||||
reader.setInput(inputStream);
|
||||
String format = reader.getFormatName().toLowerCase();
|
||||
|
||||
// Create temporary canvas
|
||||
int originalWidth = reader.getWidth(0);
|
||||
int originalHeight = reader.getHeight(0);
|
||||
BufferedImage tmpImage = new BufferedImage(originalWidth, originalHeight, BufferedImage.TYPE_4BYTE_ABGR);
|
||||
Graphics2D tmpGraphics = tmpImage.createGraphics();
|
||||
tmpGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
|
||||
// Create temporary scaled canvas (for resizing)
|
||||
BufferedImage tmpScaledImage = new BufferedImage(widthInPixels, heightInPixels, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D tmpScaledGraphics = tmpScaledImage.createGraphics();
|
||||
tmpScaledGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
|
||||
// Read images from file
|
||||
for (int step=0; step<FakeImage.MAX_STEPS; ++step) {
|
||||
try {
|
||||
// Extract step metadata
|
||||
int imageLeft = 0;
|
||||
int imageTop = 0;
|
||||
boolean disposePrevious = false;
|
||||
if (format.equals("gif")) {
|
||||
IIOMetadata metadata = reader.getImageMetadata(step);
|
||||
String formatName = metadata.getNativeMetadataFormatName();
|
||||
IIOMetadataNode metadataRoot = (IIOMetadataNode) metadata.getAsTree(formatName);
|
||||
for (int i=0; i<metadataRoot.getLength(); ++i) {
|
||||
String nodeName = metadataRoot.item(i).getNodeName();
|
||||
if (nodeName.equalsIgnoreCase("ImageDescriptor")) {
|
||||
IIOMetadataNode descriptorNode = (IIOMetadataNode) metadataRoot.item(i);
|
||||
imageLeft = Integer.parseInt(descriptorNode.getAttribute("imageLeftPosition"));
|
||||
imageTop = Integer.parseInt(descriptorNode.getAttribute("imageTopPosition"));
|
||||
} else if (nodeName.equalsIgnoreCase("GraphicControlExtension")) {
|
||||
IIOMetadataNode controlExtensionNode = (IIOMetadataNode) metadataRoot.item(i);
|
||||
int delay = Integer.parseInt(controlExtensionNode.getAttribute("delayTime"));
|
||||
delays.compute(delay, (__, count) -> (count == null) ? 1 : count + 1);
|
||||
disposePrevious = controlExtensionNode.getAttribute("disposalMethod").startsWith("restore");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear temporary canvases (if needed)
|
||||
if (disposePrevious) {
|
||||
tmpGraphics.clearRect(0, 0, originalWidth, originalHeight);
|
||||
tmpScaledGraphics.clearRect(0, 0, widthInPixels, heightInPixels);
|
||||
}
|
||||
|
||||
// Paint step image over temporary canvas
|
||||
BufferedImage image = reader.read(step);
|
||||
tmpGraphics.drawImage(image, imageLeft, imageTop, null);
|
||||
image.flush();
|
||||
|
||||
// Resize image and get pixels
|
||||
tmpScaledGraphics.drawImage(tmpImage, 0, 0, widthInPixels, heightInPixels, null);
|
||||
int[] rgbaPixels = ((DataBufferInt) tmpScaledImage.getRaster().getDataBuffer()).getData();
|
||||
|
||||
// Convert RGBA pixels to Minecraft color indexes
|
||||
byte[] renderedImage = new byte[widthInPixels * heightInPixels];
|
||||
IntStream.range(0, rgbaPixels.length)
|
||||
.parallel()
|
||||
.forEach(pixelIndex -> renderedImage[pixelIndex] = FakeMap.pixelToIndex(rgbaPixels[pixelIndex]));
|
||||
renderedImages.add(renderedImage);
|
||||
} catch (IndexOutOfBoundsException __) {
|
||||
// No more steps to read
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Free resources
|
||||
reader.dispose();
|
||||
tmpGraphics.dispose();
|
||||
tmpImage.flush();
|
||||
tmpScaledGraphics.dispose();
|
||||
tmpScaledImage.flush();
|
||||
}
|
||||
|
||||
// Get most occurring delay (mode)
|
||||
int delay = 0;
|
||||
if (renderedImages.size() > 1) {
|
||||
delay = Collections.max(delays.entrySet(), Map.Entry.comparingByValue()).getKey();
|
||||
delay = Math.round(delay * 0.2f); // (delay * 10) / 50
|
||||
delay = Math.min(Math.max(delay, FakeImage.MIN_DELAY), FakeImage.MAX_DELAY);
|
||||
}
|
||||
|
||||
// Instantiate fake maps from image steps
|
||||
FakeMap[][][] maps = new FakeMap[width][height][renderedImages.size()];
|
||||
IntStream.range(0, renderedImages.size()).forEach(i -> {
|
||||
for (int col=0; col<width; col++) {
|
||||
for (int row=0; row<height; row++) {
|
||||
maps[col][row][i] = new FakeMap(
|
||||
renderedImages.get(i),
|
||||
widthInPixels,
|
||||
col*FakeMap.DIMENSION,
|
||||
row*FakeMap.DIMENSION
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update instance state
|
||||
this.maps = maps;
|
||||
this.delay = delay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to write data to disk
|
||||
*/
|
||||
private void tryToWriteToDisk() {
|
||||
mkdirs();
|
||||
try (RandomAccessFile stream = write()) {
|
||||
int numOfSteps = maps[0][0].length;
|
||||
|
||||
// Add file header
|
||||
stream.write(CACHE_SIGNATURE); // "YMP" signature
|
||||
stream.write(CACHE_VERSION); // Format version
|
||||
stream.write(numOfSteps & 0xff); // Number of animation steps (first byte)
|
||||
stream.write((numOfSteps >> 8) & 0xff); // Number of animation steps (second byte)
|
||||
if (numOfSteps > 1) {
|
||||
stream.write(delay);
|
||||
}
|
||||
|
||||
// Add pixels
|
||||
//noinspection ForLoopReplaceableByForEach
|
||||
for (int col=0; col<maps.length; ++col) {
|
||||
for (int row=0; row<maps[0].length; ++row) {
|
||||
for (int step=0; step<numOfSteps; ++step) {
|
||||
stream.write(maps[col][row][step].getPixels());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Failed to write to cache file \"" + path + "\"", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,326 +0,0 @@
|
||||
package io.josemmo.bukkit.plugin.storage;
|
||||
|
||||
import com.sun.nio.file.ExtendedWatchEventModifier;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Service for detecting file events inside a given directory.
|
||||
* <p>
|
||||
* It supports recursive storage (<i>e.g.,</i> nested directories) and watches for file system changes in realtime
|
||||
* when supported by the OS.
|
||||
*/
|
||||
public abstract class FileSystemWatcher {
|
||||
private static final int MAX_DEPTH = 32;
|
||||
private static final int POLLING_INTERVAL = 4000;
|
||||
private static final String PROBE_FILENAME = ".inotify_test";
|
||||
private static final Logger LOGGER = Logger.getLogger("FileSystemWatcher");
|
||||
protected final Path basePath;
|
||||
/** Map of existing directories with the files they contain and their last modification timestamps */
|
||||
private final SortedMap<Path, Map<Path, Long>> fileTree = new TreeMap<>();
|
||||
private @Nullable Thread watcherThread;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param basePath Base path
|
||||
*/
|
||||
public FileSystemWatcher(@NotNull Path basePath) {
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start watcher
|
||||
* @throws RuntimeException if failed to start
|
||||
*/
|
||||
protected void start() throws RuntimeException {
|
||||
// Prevent initializing more than once
|
||||
if (watcherThread != null) {
|
||||
throw new RuntimeException("File system watcher is already running");
|
||||
}
|
||||
|
||||
// Perform initial scan
|
||||
scan();
|
||||
|
||||
// Start watching files
|
||||
watcherThread = new WatcherThread();
|
||||
watcherThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop watcher
|
||||
*/
|
||||
protected void stop() {
|
||||
if (watcherThread != null) {
|
||||
watcherThread.interrupt();
|
||||
watcherThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On file created
|
||||
* @param path File path
|
||||
*/
|
||||
protected abstract void onFileCreated(@NotNull Path path);
|
||||
|
||||
/**
|
||||
* On file modified
|
||||
* @param path File path
|
||||
*/
|
||||
protected abstract void onFileModified(@NotNull Path path);
|
||||
|
||||
/**
|
||||
* On file deleted
|
||||
* @param path File path
|
||||
*/
|
||||
protected abstract void onFileDeleted(@NotNull Path path);
|
||||
|
||||
/**
|
||||
* Scan base directory recursively
|
||||
*/
|
||||
private void scan() {
|
||||
synchronized (fileTree) {
|
||||
// Assume all directories and files have been deleted, will discard existing items afterward
|
||||
Set<Path> deletedDirectories = new HashSet<>(fileTree.keySet());
|
||||
Set<Path> deletedFiles = fileTree.values().stream()
|
||||
.map(Map::keySet)
|
||||
.flatMap(Set::stream)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// Traverse file tree
|
||||
Set<FileVisitOption> options = EnumSet.noneOf(FileVisitOption.class);
|
||||
try {
|
||||
Files.walkFileTree(basePath, options, MAX_DEPTH, new SimpleFileVisitor<Path>() {
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) {
|
||||
fileTree.putIfAbsent(path, new HashMap<>());
|
||||
deletedDirectories.remove(path);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
|
||||
Map<Path, Long> subtree = fileTree.get(path.getParent());
|
||||
Long oldModifiedAt = subtree.get(path);
|
||||
long newModifiedAt = attrs.lastModifiedTime().toMillis();
|
||||
if (oldModifiedAt == null) {
|
||||
subtree.put(path, newModifiedAt);
|
||||
onFileCreated(path);
|
||||
} else if (newModifiedAt > oldModifiedAt) {
|
||||
subtree.put(path, newModifiedAt);
|
||||
onFileModified(path);
|
||||
}
|
||||
deletedFiles.remove(path);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Failed to list files in directory", e);
|
||||
}
|
||||
|
||||
// Process deleted files and directories
|
||||
for (Path path : deletedFiles) {
|
||||
fileTree.get(path.getParent()).remove(path);
|
||||
onFileDeleted(path);
|
||||
}
|
||||
for (Path path : deletedDirectories) {
|
||||
fileTree.remove(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class WatcherThread extends Thread {
|
||||
private final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ROOT).contains("win");
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isInotifySupported()) {
|
||||
runWithFileSystemEvents();
|
||||
} else {
|
||||
LOGGER.warning("Device does not support inotify, detection of file changes will be slower");
|
||||
runWithPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Is inotify supported
|
||||
* @return Whether inotify is supported
|
||||
*/
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
private boolean isInotifySupported() {
|
||||
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
|
||||
// Start listening for events
|
||||
basePath.register(
|
||||
watchService,
|
||||
StandardWatchEventKinds.ENTRY_CREATE,
|
||||
StandardWatchEventKinds.ENTRY_DELETE,
|
||||
StandardWatchEventKinds.ENTRY_MODIFY
|
||||
);
|
||||
|
||||
// Create and delete probe directory
|
||||
File testFile = basePath.resolve(PROBE_FILENAME).toFile();
|
||||
testFile.mkdir();
|
||||
testFile.delete();
|
||||
|
||||
// Check that at least one event was emitted
|
||||
WatchKey watchKey = watchService.poll();
|
||||
return (watchKey != null && !watchKey.pollEvents().isEmpty());
|
||||
} catch (IOException __) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run with polling
|
||||
*/
|
||||
@SuppressWarnings({"InfiniteLoopStatement", "BusyWait"})
|
||||
private void runWithPolling() {
|
||||
try {
|
||||
while (true) {
|
||||
Thread.sleep(POLLING_INTERVAL);
|
||||
scan();
|
||||
}
|
||||
} catch (InterruptedException __) {
|
||||
// Silently ignore exception, this is expected when service shuts down
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run with file system events
|
||||
*/
|
||||
private void runWithFileSystemEvents() {
|
||||
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
|
||||
// Register initial directories
|
||||
synchronized (fileTree) {
|
||||
for (Path path : fileTree.keySet()) {
|
||||
registerDirectory(watchService, path);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen for events
|
||||
WatchKey key;
|
||||
try {
|
||||
while ((key = watchService.take()) != null) {
|
||||
for (WatchEvent<?> event : key.pollEvents()) {
|
||||
WatchEvent.Kind<?> kind = event.kind();
|
||||
Path keyPath = (Path) key.watchable();
|
||||
Path path = keyPath.resolve((Path) event.context());
|
||||
handleWatchEvent(watchService, path, kind);
|
||||
}
|
||||
key.reset();
|
||||
}
|
||||
} catch (ClosedWatchServiceException | InterruptedException __) {
|
||||
// Silently ignore exception, this is expected when service shuts down
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Unexpected error at watch service", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle watch event
|
||||
* @param watchService Watch service
|
||||
* @param path File or directory path
|
||||
* @param kind Event kind
|
||||
*/
|
||||
private void handleWatchEvent(@NotNull WatchService watchService, @NotNull Path path, @NotNull WatchEvent.Kind<?> kind) {
|
||||
synchronized (fileTree) {
|
||||
Map<Path, Long> subtree = fileTree.computeIfAbsent(path.getParent(), k -> new HashMap<>());
|
||||
|
||||
// Handle deletion of files and directories
|
||||
if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
|
||||
if (subtree.containsKey(path)) {
|
||||
subtree.remove(path);
|
||||
onFileDeleted(path);
|
||||
} else {
|
||||
unregisterDirectory(path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle creation of directories
|
||||
if (path.toFile().isDirectory()) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
|
||||
registerDirectory(watchService, path);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle creation and modification of files
|
||||
// NOTE: in Windows, some file creation events are reported as modifications
|
||||
Long oldModifiedAt = subtree.get(path);
|
||||
long newModifiedAt = path.toFile().lastModified();
|
||||
if (oldModifiedAt == null) {
|
||||
subtree.put(path, newModifiedAt);
|
||||
onFileCreated(path);
|
||||
} else if (newModifiedAt > oldModifiedAt) {
|
||||
subtree.put(path, newModifiedAt);
|
||||
onFileModified(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register directory
|
||||
* @param watchService Watch service
|
||||
* @param path Directory path
|
||||
*/
|
||||
private void registerDirectory(@NotNull WatchService watchService, @NotNull Path path) {
|
||||
// Windows supports listing to events in the entire file tree,
|
||||
// in that case only allow registering the listener on the base path
|
||||
if (IS_WINDOWS && !path.equals(basePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start watching directory for events
|
||||
try {
|
||||
WatchEvent.Kind<?>[] events = new WatchEvent.Kind[]{
|
||||
StandardWatchEventKinds.ENTRY_CREATE,
|
||||
StandardWatchEventKinds.ENTRY_DELETE,
|
||||
StandardWatchEventKinds.ENTRY_MODIFY
|
||||
};
|
||||
WatchEvent.Modifier[] modifiers = IS_WINDOWS ?
|
||||
new WatchEvent.Modifier[]{ExtendedWatchEventModifier.FILE_TREE} :
|
||||
new WatchEvent.Modifier[0];
|
||||
path.register(watchService, events, modifiers);
|
||||
LOGGER.fine("Started watching directory at \"" + path + "\"");
|
||||
} catch (IOException e) {
|
||||
LOGGER.severe("Failed to register directory", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister directory
|
||||
* @param path Directory path
|
||||
*/
|
||||
private void unregisterDirectory(@NotNull Path path) {
|
||||
synchronized (fileTree) {
|
||||
if (!fileTree.containsKey(path)) {
|
||||
// Already unregistered, can skip work
|
||||
return;
|
||||
}
|
||||
boolean foundFirst = false;
|
||||
Iterator<Map.Entry<Path, Map<Path, Long>>> iter = fileTree.entrySet().iterator();
|
||||
while (iter.hasNext()) {
|
||||
Map.Entry<Path, Map<Path, Long>> entry = iter.next();
|
||||
if (entry.getKey().startsWith(path)) {
|
||||
for (Path childPath : entry.getValue().keySet()) {
|
||||
onFileDeleted(childPath);
|
||||
}
|
||||
foundFirst = true;
|
||||
iter.remove();
|
||||
} else if (foundFirst) {
|
||||
// We can break early because set is alphabetically sorted by key
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +1,204 @@
|
||||
package io.josemmo.bukkit.plugin.storage;
|
||||
|
||||
import com.comphenix.protocol.wrappers.Pair;
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeImage;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeItemFrame;
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeMap;
|
||||
import io.josemmo.bukkit.plugin.renderer.FakeMapsContainer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageReader;
|
||||
import javax.imageio.metadata.IIOMetadata;
|
||||
import javax.imageio.metadata.IIOMetadataNode;
|
||||
import javax.imageio.stream.ImageInputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.DataBufferInt;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class ImageFile extends SynchronizedFile {
|
||||
private static final Logger LOGGER = Logger.getLogger("ImageFile");
|
||||
public class ImageFile {
|
||||
public static final String CACHE_EXT = "cache";
|
||||
public static final byte[] CACHE_SIGNATURE = new byte[] {0x59, 0x4d, 0x50}; // "YMP"
|
||||
public static final int CACHE_VERSION = 1;
|
||||
private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private final ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();
|
||||
private final Map<String, CachedMapsFile> cache = new HashMap<>();
|
||||
private final Map<String, FakeMapsContainer> cache = new HashMap<>();
|
||||
private final Map<String, Set<FakeImage>> subscribers = new HashMap<>();
|
||||
private final String filename;
|
||||
private @Nullable Dimension size;
|
||||
private final String name;
|
||||
private final String path;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param filename Image filename
|
||||
* @param path Path to image file
|
||||
* @param name Image file name
|
||||
* @param path Path to image file
|
||||
*/
|
||||
protected ImageFile(@NotNull String filename, @NotNull Path path) {
|
||||
super(path);
|
||||
this.filename = filename;
|
||||
protected ImageFile(@NotNull String name, @NotNull String path) {
|
||||
this.name = name;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image filename
|
||||
* @return Image filename
|
||||
* Get image file name
|
||||
* @return Image file name
|
||||
*/
|
||||
public @NotNull String getFilename() {
|
||||
return filename;
|
||||
public @NotNull String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image reader
|
||||
* @return Image reader instance
|
||||
* @throws IOException if failed to get suitable image reader
|
||||
*/
|
||||
private @NotNull ImageReader getImageReader() throws IOException {
|
||||
ImageInputStream inputStream = ImageIO.createImageInputStream(new File(path));
|
||||
ImageReader reader = ImageIO.getImageReaders(inputStream).next();
|
||||
reader.setInput(inputStream);
|
||||
return reader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render images using Minecraft palette
|
||||
* @param width New width in pixels
|
||||
* @param height New height in pixels
|
||||
* @return Pair of bi-dimensional array of Minecraft images (step, pixel index) and delay between steps
|
||||
* @throws IOException if failed to render images from file
|
||||
*/
|
||||
private @NotNull Pair<byte[][], Integer> renderImages(int width, int height) throws IOException {
|
||||
ImageReader reader = getImageReader();
|
||||
String format = reader.getFormatName().toLowerCase();
|
||||
|
||||
// Create temporary canvas
|
||||
int originalWidth = reader.getWidth(0);
|
||||
int originalHeight = reader.getHeight(0);
|
||||
BufferedImage tmpImage = new BufferedImage(originalWidth, originalHeight, BufferedImage.TYPE_4BYTE_ABGR);
|
||||
Graphics2D tmpGraphics = tmpImage.createGraphics();
|
||||
tmpGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
|
||||
// Create temporary scaled canvas (for resizing)
|
||||
BufferedImage tmpScaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D tmpScaledGraphics = tmpScaledImage.createGraphics();
|
||||
tmpScaledGraphics.setBackground(new Color(0, 0, 0, 0));
|
||||
|
||||
// Read images from file
|
||||
List<byte[]> renderedImages = new ArrayList<>();
|
||||
Map<Integer, Integer> delays = new HashMap<>();
|
||||
for (int step=0; step<FakeImage.MAX_STEPS; ++step) {
|
||||
try {
|
||||
// Extract step metadata
|
||||
int imageLeft = 0;
|
||||
int imageTop = 0;
|
||||
boolean disposePrevious = false;
|
||||
if (format.equals("gif")) {
|
||||
IIOMetadata metadata = reader.getImageMetadata(step);
|
||||
IIOMetadataNode metadataRoot = (IIOMetadataNode) metadata.getAsTree(metadata.getNativeMetadataFormatName());
|
||||
for (int i=0; i<metadataRoot.getLength(); ++i) {
|
||||
String nodeName = metadataRoot.item(i).getNodeName();
|
||||
if (nodeName.equalsIgnoreCase("ImageDescriptor")) {
|
||||
IIOMetadataNode descriptorNode = (IIOMetadataNode) metadataRoot.item(i);
|
||||
imageLeft = Integer.parseInt(descriptorNode.getAttribute("imageLeftPosition"));
|
||||
imageTop = Integer.parseInt(descriptorNode.getAttribute("imageTopPosition"));
|
||||
} else if (nodeName.equalsIgnoreCase("GraphicControlExtension")) {
|
||||
IIOMetadataNode controlExtensionNode = (IIOMetadataNode) metadataRoot.item(i);
|
||||
int delay = Integer.parseInt(controlExtensionNode.getAttribute("delayTime"));
|
||||
delays.compute(delay, (__, count) -> (count == null) ? 1 : count + 1);
|
||||
disposePrevious = controlExtensionNode.getAttribute("disposalMethod").startsWith("restore");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear temporary canvases (if needed)
|
||||
if (disposePrevious) {
|
||||
tmpGraphics.clearRect(0, 0, originalWidth, originalHeight);
|
||||
tmpScaledGraphics.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// Paint step image over temporary canvas
|
||||
BufferedImage image = reader.read(step);
|
||||
tmpGraphics.drawImage(image, imageLeft, imageTop, null);
|
||||
image.flush();
|
||||
|
||||
// Resize image and get pixels
|
||||
tmpScaledGraphics.drawImage(tmpImage, 0, 0, width, height, null);
|
||||
int[] rgbaPixels = ((DataBufferInt) tmpScaledImage.getRaster().getDataBuffer()).getData();
|
||||
|
||||
// Convert RGBA pixels to Minecraft color indexes
|
||||
byte[] renderedImage = new byte[width*height];
|
||||
IntStream.range(0, rgbaPixels.length).parallel().forEach(pixelIndex -> {
|
||||
renderedImage[pixelIndex] = FakeMap.pixelToIndex(rgbaPixels[pixelIndex]);
|
||||
});
|
||||
renderedImages.add(renderedImage);
|
||||
} catch (IndexOutOfBoundsException __) {
|
||||
// No more steps to read
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Get most occurring delay (mode)
|
||||
int delay = 0;
|
||||
if (renderedImages.size() > 1) {
|
||||
delay = Collections.max(delays.entrySet(), Map.Entry.comparingByValue()).getKey();
|
||||
delay = Math.round(delay * 0.2f); // (delay * 10) / 50
|
||||
delay = Math.min(Math.max(delay, FakeImage.MIN_DELAY), FakeImage.MAX_DELAY);
|
||||
}
|
||||
|
||||
// Free resources
|
||||
reader.dispose();
|
||||
tmpGraphics.dispose();
|
||||
tmpImage.flush();
|
||||
tmpScaledGraphics.dispose();
|
||||
tmpScaledImage.flush();
|
||||
|
||||
return new Pair<>(renderedImages.toArray(new byte[0][0]), delay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get original size in pixels
|
||||
* @return Dimension instance or NULL if not a valid image file
|
||||
*/
|
||||
public synchronized @Nullable Dimension getSize() {
|
||||
if (size != null) {
|
||||
return size;
|
||||
}
|
||||
try (ImageInputStream inputStream = ImageIO.createImageInputStream(read())) {
|
||||
ImageReader reader = ImageIO.getImageReaders(inputStream).next();
|
||||
reader.setInput(inputStream);
|
||||
size = new Dimension(reader.getWidth(0), reader.getHeight(0));
|
||||
public @Nullable Dimension getSize() {
|
||||
try {
|
||||
ImageReader reader = getImageReader();
|
||||
Dimension dimension = new Dimension(reader.getWidth(0), reader.getHeight(0));
|
||||
reader.dispose();
|
||||
return size;
|
||||
} catch (IOException | RuntimeException e) {
|
||||
return dimension;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maps and subscribe to them
|
||||
* @param subscriber Fake image instance requesting the maps
|
||||
* @return Cached maps
|
||||
* Get image last modified time
|
||||
* @return Last modified time in milliseconds, zero in case of error
|
||||
*/
|
||||
@Blocking
|
||||
public @NotNull CachedMapsFile getMapsAndSubscribe(@NotNull FakeImage subscriber) {
|
||||
public long getLastModified() {
|
||||
try {
|
||||
return Files.getLastModifiedTime(Paths.get(path)).toMillis();
|
||||
} catch (Exception __) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maps and subscribe to maps cache
|
||||
* @param subscriber Fake image instance requesting the maps
|
||||
* @return Fake maps container
|
||||
*/
|
||||
public @NotNull FakeMapsContainer getMapsAndSubscribe(@NotNull FakeImage subscriber) {
|
||||
int width = subscriber.getWidth();
|
||||
int height = subscriber.getHeight();
|
||||
String cacheKey = width + "-" + height;
|
||||
@@ -80,38 +207,191 @@ public class ImageFile extends SynchronizedFile {
|
||||
Lock lock = locks.computeIfAbsent(cacheKey, __ -> new ReentrantLock());
|
||||
lock.lock();
|
||||
|
||||
// Get cached maps without locking this instance
|
||||
CachedMapsFile maps = cache.get(cacheKey);
|
||||
if (maps == null) {
|
||||
maps = CachedMapsFile.from(this, width, height);
|
||||
// Execute code
|
||||
FakeMapsContainer container;
|
||||
try {
|
||||
container = getMapsAndSubscribe(subscriber, cacheKey, width, height);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
||||
// Update state of this instance
|
||||
synchronized (this) {
|
||||
if (cache.get(cacheKey) != maps) {
|
||||
cache.put(cacheKey, maps);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maps and subscribe to maps cache (internal)
|
||||
* @param subscriber Fake image instance requesting the maps
|
||||
* @param cacheKey Maps cache key
|
||||
* @param width Desired image width in pixels
|
||||
* @param height Desired image height in pixels
|
||||
* @return Fake maps container
|
||||
*/
|
||||
private @NotNull FakeMapsContainer getMapsAndSubscribe(
|
||||
@NotNull FakeImage subscriber,
|
||||
@NotNull String cacheKey,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
// Update subscribers for given cached maps
|
||||
subscribers.computeIfAbsent(cacheKey, __ -> new HashSet<>()).add(subscriber);
|
||||
|
||||
// Try to get maps from memory cache
|
||||
if (cache.containsKey(cacheKey)) {
|
||||
return cache.get(cacheKey);
|
||||
}
|
||||
|
||||
// Try to get maps from disk cache
|
||||
String cacheFilename = name + "." + cacheKey + "." + CACHE_EXT;
|
||||
File cacheFile = Paths.get(plugin.getStorage().getCachePath(), cacheFilename).toFile();
|
||||
if (cacheFile.isFile() && cacheFile.lastModified() >= getLastModified()) {
|
||||
try {
|
||||
FakeMapsContainer container = readMapsFromCacheFile(cacheFile, width, height);
|
||||
cache.put(cacheKey, container);
|
||||
return container;
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.info("Cache file \"" + cacheFile.getAbsolutePath() + "\" is outdated and will be overwritten");
|
||||
} catch (Exception e) {
|
||||
plugin.log(Level.WARNING, "Cache file \"" + cacheFile.getAbsolutePath() + "\" is corrupted", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate maps from original image
|
||||
FakeMapsContainer container;
|
||||
try {
|
||||
int widthInPixels = width*FakeMap.DIMENSION;
|
||||
int heightInPixels = height*FakeMap.DIMENSION;
|
||||
Pair<byte[][], Integer> res = renderImages(widthInPixels, heightInPixels);
|
||||
byte[][] images = res.getFirst();
|
||||
int delay = res.getSecond();
|
||||
|
||||
// Instantiate fake maps
|
||||
FakeMap[][][] matrix = new FakeMap[width][height][images.length];
|
||||
IntStream.range(0, images.length).forEach(i -> {
|
||||
for (int col=0; col<width; col++) {
|
||||
for (int row=0; row<height; row++) {
|
||||
matrix[col][row][i] = new FakeMap(
|
||||
images[i],
|
||||
widthInPixels,
|
||||
col*FakeMap.DIMENSION,
|
||||
row*FakeMap.DIMENSION
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Persist in disk cache
|
||||
container = new FakeMapsContainer(matrix, delay);
|
||||
try {
|
||||
writeMapsToCacheFile(container, cacheFile);
|
||||
} catch (IOException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to write to cache file \"" + cacheFile.getAbsolutePath() + "\"", e);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
container = FakeMap.getErrorMatrix(width, height);
|
||||
plugin.log(Level.SEVERE, "Failed to render image(s) from file \"" + path + "\"", e);
|
||||
}
|
||||
|
||||
// Persist in memory cache and return
|
||||
cache.put(cacheKey, container);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read maps from cache file
|
||||
* @param file Cache file
|
||||
* @param width Width in blocks
|
||||
* @param height Height in blocks
|
||||
* @return Fake maps container
|
||||
* @throws IllegalArgumentException if not a valid or outdated cache file
|
||||
* @throws IOException if failed to parse cache file
|
||||
*/
|
||||
private @NotNull FakeMapsContainer readMapsFromCacheFile(@NotNull File file, int width, int height) throws Exception {
|
||||
try (FileInputStream stream = new FileInputStream(file)) {
|
||||
// Validate file signature
|
||||
for (byte expectedByte : CACHE_SIGNATURE) {
|
||||
if ((byte) stream.read() != expectedByte) {
|
||||
throw new IllegalArgumentException("Invalid file signature");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate version number
|
||||
if ((byte) stream.read() != CACHE_VERSION) {
|
||||
throw new IllegalArgumentException("Incompatible file format version");
|
||||
}
|
||||
|
||||
// Get number of animation steps
|
||||
int numOfSteps = stream.read() | (stream.read() << 8);
|
||||
if (numOfSteps < 1 || numOfSteps > FakeImage.MAX_STEPS) {
|
||||
throw new IOException("Invalid number of animation steps: " + numOfSteps);
|
||||
}
|
||||
|
||||
// Get delay between steps
|
||||
int delay = 0;
|
||||
if (numOfSteps > 1) {
|
||||
delay = stream.read();
|
||||
if (delay < FakeImage.MIN_DELAY || delay > FakeImage.MAX_DELAY) {
|
||||
throw new IOException("Invalid delay between steps: " + delay);
|
||||
}
|
||||
}
|
||||
|
||||
// Read pixels
|
||||
FakeMap[][][] maps = new FakeMap[width][height][numOfSteps];
|
||||
for (int col=0; col<width; ++col) {
|
||||
for (int row=0; row<height; ++row) {
|
||||
for (int step=0; step<numOfSteps; ++step) {
|
||||
byte[] buffer = new byte[FakeMap.DIMENSION*FakeMap.DIMENSION];
|
||||
stream.read(buffer);
|
||||
maps[col][row][step] = new FakeMap(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new FakeMapsContainer(maps, delay);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write maps to cache file
|
||||
* @param container Fake maps container
|
||||
* @param file Cache file
|
||||
* @throws IOException if failed to write to cache file
|
||||
*/
|
||||
private void writeMapsToCacheFile(@NotNull FakeMapsContainer container, @NotNull File file) throws IOException {
|
||||
try (FileOutputStream stream = new FileOutputStream(file)) {
|
||||
FakeMap[][][] maps = container.getFakeMaps();
|
||||
int numOfSteps = maps[0][0].length;
|
||||
|
||||
// Add file header
|
||||
stream.write(CACHE_SIGNATURE); // "YMP" signature
|
||||
stream.write(CACHE_VERSION); // Format version
|
||||
stream.write(numOfSteps & 0xff); // Number of animation steps (first byte)
|
||||
stream.write((numOfSteps >> 8) & 0xff); // Number of animation steps (second byte)
|
||||
if (numOfSteps > 1) {
|
||||
stream.write(container.getDelay());
|
||||
}
|
||||
|
||||
// Add pixels
|
||||
for (int col=0; col<maps.length; ++col) {
|
||||
for (int row=0; row<maps[0].length; ++row) {
|
||||
for (int step=0; step<numOfSteps; ++step) {
|
||||
stream.write(maps[col][row][step].getPixels());
|
||||
}
|
||||
}
|
||||
}
|
||||
subscribers.computeIfAbsent(cacheKey, __ -> new HashSet<>()).add(subscriber);
|
||||
lock.unlock();
|
||||
locks.remove(cacheKey);
|
||||
return maps;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from memory cache
|
||||
* <p>
|
||||
* This method is called by {@link FakeImage} instances when they get invalidated by a world area change.
|
||||
* By notifying their respective source {@link ImageFile}, the latter can clear cached maps from memory when no
|
||||
* more {@link FakeItemFrame}s are using them.
|
||||
* This method is called by FakeImage instances when they get invalidated by a WorldArea.
|
||||
* By notifying their respective source ImageFile, the latter can clear cached maps from memory when no more
|
||||
* FakeItemFrames are using them.
|
||||
* @param subscriber Fake image instance
|
||||
*/
|
||||
public synchronized void unsubscribe(@NotNull FakeImage subscriber) {
|
||||
String cacheKey = subscriber.getWidth() + "-" + subscriber.getHeight();
|
||||
if (!subscribers.containsKey(cacheKey)) {
|
||||
// Not subscribed to this image file
|
||||
return;
|
||||
}
|
||||
if (!subscribers.containsKey(cacheKey)) return;
|
||||
|
||||
// Remove subscriber
|
||||
Set<FakeImage> currentSubscribers = subscribers.get(cacheKey);
|
||||
@@ -121,19 +401,31 @@ public class ImageFile extends SynchronizedFile {
|
||||
if (currentSubscribers.isEmpty()) {
|
||||
subscribers.remove(cacheKey);
|
||||
cache.remove(cacheKey);
|
||||
LOGGER.fine("Invalidated cached maps \"" + cacheKey + "\" in ImageFile#(" + filename + ")");
|
||||
plugin.fine("Invalidated cached maps \"" + cacheKey + "\" in ImageFile#(" + name + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache
|
||||
* <p>
|
||||
* Removes all references to cached map instances.
|
||||
* Removes all references to pre-generated map sets.
|
||||
* This way, next time an image is requested to be rendered, maps will be regenerated.
|
||||
*/
|
||||
public synchronized void invalidate() {
|
||||
size = null;
|
||||
cache.clear();
|
||||
CachedMapsFile.deleteAll(this);
|
||||
|
||||
// Delete disk cache files
|
||||
File[] files = Paths.get(plugin.getStorage().getCachePath()).toFile().listFiles((__, filename) -> {
|
||||
return filename.matches(Pattern.quote(name) + "\\.[0-9]+-[0-9]+\\." + CACHE_EXT);
|
||||
});
|
||||
if (files == null) {
|
||||
plugin.warning("An error occurred when listing cache files for image \"" + name + "\"");
|
||||
return;
|
||||
}
|
||||
for (File file : files) {
|
||||
if (!file.delete()) {
|
||||
plugin.warning("Failed to delete cache file \"" + file.getAbsolutePath() + "\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +1,42 @@
|
||||
package io.josemmo.bukkit.plugin.storage;
|
||||
|
||||
import io.josemmo.bukkit.plugin.utils.Logger;
|
||||
import io.josemmo.bukkit.plugin.utils.Permissions;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
import java.util.Objects;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Service for keeping track of image images.
|
||||
* <p>
|
||||
* All files are indexed based on their <b>filename</b>.
|
||||
* Due to recursion, filenames can contain forward slashes (<i>i.e.,</i> "/") and act as relative paths to the base
|
||||
* directory.
|
||||
*/
|
||||
public class ImageStorage extends FileSystemWatcher {
|
||||
private static final Logger LOGGER = Logger.getLogger("ImageStorage");
|
||||
/** Map of registered files indexed by filename */
|
||||
private final SortedMap<String, ImageFile> files = new TreeMap<>();
|
||||
private final Path cachePath;
|
||||
private final String allowedPaths;
|
||||
public class ImageStorage {
|
||||
static public final long POLLING_INTERVAL = 20L * 5; // In server ticks
|
||||
static private final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private final String basePath;
|
||||
private final String cachePath;
|
||||
private final SortedMap<String, ImageFile> cachedImages = new TreeMap<>();
|
||||
private BukkitTask task;
|
||||
private WatchService watchService;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param basePath Path to directory containing the images
|
||||
* @param cachePath Path to directory containing the cached image maps
|
||||
* @param allowedPaths Allowed paths pattern
|
||||
* @param basePath Path to directory containing the images
|
||||
* @param cachePath Path to directory containing the cached image maps
|
||||
*/
|
||||
public ImageStorage(@NotNull Path basePath, @NotNull Path cachePath, @NotNull String allowedPaths) {
|
||||
super(basePath);
|
||||
public ImageStorage(@NotNull String basePath, @NotNull String cachePath) {
|
||||
this.basePath = basePath;
|
||||
this.cachePath = cachePath;
|
||||
this.allowedPaths = allowedPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get base path
|
||||
* @return Base path
|
||||
*/
|
||||
public @NotNull Path getBasePath() {
|
||||
public @NotNull String getBasePath() {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
@@ -50,35 +44,94 @@ public class ImageStorage extends FileSystemWatcher {
|
||||
* Get cache path
|
||||
* @return Cache path
|
||||
*/
|
||||
public @NotNull Path getCachePath() {
|
||||
public @NotNull String getCachePath() {
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start service
|
||||
* @throws RuntimeException if failed to start watch service
|
||||
* Start instance
|
||||
* @throws SecurityException if failed to access filesystem
|
||||
* @throws Exception if failed to start watch service
|
||||
*/
|
||||
@Override
|
||||
public void start() throws RuntimeException {
|
||||
// Create base directories if necessary
|
||||
if (basePath.toFile().mkdirs()) {
|
||||
LOGGER.info("Created images directory as it did not exist");
|
||||
public void start() throws Exception {
|
||||
// Create directories if necessary
|
||||
File directory = new File(basePath);
|
||||
if (directory.mkdirs()) {
|
||||
plugin.info("Created images directory as it did not exist");
|
||||
}
|
||||
if (cachePath.toFile().mkdirs()) {
|
||||
LOGGER.info("Created cache directory as it did not exist");
|
||||
if (new File(cachePath).mkdirs()) {
|
||||
plugin.info("Created cache directory as it did not exist");
|
||||
}
|
||||
|
||||
// Start file system watcher
|
||||
super.start();
|
||||
LOGGER.fine("Found " + files.size() + " file(s) in images directory");
|
||||
// Do initial directory listing
|
||||
for (File file : Objects.requireNonNull(directory.listFiles())) {
|
||||
if (file.isDirectory()) continue;
|
||||
String filename = file.getName();
|
||||
cachedImages.put(filename, new ImageFile(filename, file.getAbsolutePath()));
|
||||
}
|
||||
plugin.fine("Found " + cachedImages.size() + " file(s) in images directory");
|
||||
|
||||
// Prepare watch service
|
||||
watchService = FileSystems.getDefault().newWatchService();
|
||||
Path directoryPath = directory.toPath();
|
||||
directoryPath.register(
|
||||
watchService,
|
||||
StandardWatchEventKinds.ENTRY_CREATE,
|
||||
StandardWatchEventKinds.ENTRY_DELETE,
|
||||
StandardWatchEventKinds.ENTRY_MODIFY
|
||||
);
|
||||
|
||||
// Start watching for changes
|
||||
task = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, () -> {
|
||||
WatchKey watchKey = watchService.poll();
|
||||
if (watchKey == null) return;
|
||||
|
||||
watchKey.pollEvents().forEach(event -> {
|
||||
WatchEvent.Kind<?> kind = event.kind();
|
||||
File file = directoryPath.resolve((Path) event.context()).toFile();
|
||||
if (file.isDirectory()) return;
|
||||
|
||||
String filename = file.getName();
|
||||
synchronized (this) {
|
||||
if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
|
||||
ImageFile imageFile = cachedImages.get(filename);
|
||||
if (imageFile != null) {
|
||||
imageFile.invalidate();
|
||||
cachedImages.remove(filename);
|
||||
}
|
||||
plugin.fine("Detected file deletion at " + filename);
|
||||
} else if (cachedImages.containsKey(filename)) {
|
||||
cachedImages.get(filename).invalidate();
|
||||
plugin.fine("Detected file update at " + filename);
|
||||
} else {
|
||||
cachedImages.put(filename, new ImageFile(filename, file.getAbsolutePath()));
|
||||
plugin.fine("Detected file creation at " + filename);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watchKey.reset();
|
||||
}, POLLING_INTERVAL, POLLING_INTERVAL);
|
||||
plugin.fine("Started watching for file changes in images directory");
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop service
|
||||
* Stop instance
|
||||
*/
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
// Cancel async task
|
||||
if (task != null) {
|
||||
task.cancel();
|
||||
}
|
||||
|
||||
// Close watch service
|
||||
if (watchService != null) {
|
||||
try {
|
||||
watchService.close();
|
||||
} catch (IOException e) {
|
||||
plugin.log(Level.WARNING, "Failed to close watch service", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,70 +139,15 @@ public class ImageStorage extends FileSystemWatcher {
|
||||
* @return Number of images
|
||||
*/
|
||||
public synchronized int size() {
|
||||
return files.size();
|
||||
return cachedImages.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image filenames
|
||||
* @param sender Sender instance to filter only allowed images
|
||||
* @return Allowed images
|
||||
* Get all image filenames
|
||||
* @return Sorted array of image filenames
|
||||
*/
|
||||
public synchronized @NotNull List<String> getFilenames(@NotNull CommandSender sender) {
|
||||
List<String> response = new ArrayList<>();
|
||||
for (String filename : files.keySet()) {
|
||||
if (isPathAllowed(filename, sender)) {
|
||||
response.add(filename);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is path allowed
|
||||
* @param path Path instance
|
||||
* @param sender Sender instance
|
||||
* @return Whether sender is allowed to access path
|
||||
*/
|
||||
public boolean isPathAllowed(@NotNull Path path, @NotNull CommandSender sender) {
|
||||
return isPathAllowed(pathToFilename(path), sender);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is path allowed
|
||||
* @param path Path relative to {@link ImageStorage#basePath}
|
||||
* @param sender Sender instance
|
||||
* @return Whether sender is allowed to access path
|
||||
*/
|
||||
public boolean isPathAllowed(@NotNull String path, @NotNull CommandSender sender) {
|
||||
// Find allowed paths pattern
|
||||
String rawPattern = null;
|
||||
if (sender instanceof Player) {
|
||||
rawPattern = Permissions.getVariable("yamipa-allowed-paths", (Player) sender);
|
||||
}
|
||||
if (rawPattern == null) {
|
||||
rawPattern = allowedPaths;
|
||||
}
|
||||
if (rawPattern.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Replace special tokens in pattern
|
||||
if (sender instanceof Player) {
|
||||
Player player = (Player) sender;
|
||||
rawPattern = rawPattern.replaceAll("#player#", Matcher.quoteReplacement(Pattern.quote(player.getName())));
|
||||
rawPattern = rawPattern.replaceAll("#uuid#", player.getUniqueId().toString());
|
||||
} else {
|
||||
rawPattern = rawPattern.replaceAll("#player#", ".+");
|
||||
rawPattern = rawPattern.replaceAll("#uuid#", ".+");
|
||||
}
|
||||
|
||||
// Perform partial match against pattern
|
||||
try {
|
||||
return Pattern.compile(rawPattern).matcher(path).find();
|
||||
} catch (PatternSyntaxException __) {
|
||||
LOGGER.warning("Invalid allowed paths pattern: " + rawPattern);
|
||||
return false;
|
||||
}
|
||||
public synchronized @NotNull String[] getAllFilenames() {
|
||||
return cachedImages.keySet().toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,53 +156,6 @@ public class ImageStorage extends FileSystemWatcher {
|
||||
* @return Image instance or NULL if not found
|
||||
*/
|
||||
public synchronized @Nullable ImageFile get(@NotNull String filename) {
|
||||
return files.get(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert path to filename
|
||||
* @param path File path
|
||||
* @return Relative path used for indexing
|
||||
*/
|
||||
private @NotNull String pathToFilename(@NotNull Path path) {
|
||||
return basePath.relativize(path).toString().replaceAll("\\\\", "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* On file created
|
||||
* @param path File path
|
||||
*/
|
||||
protected synchronized void onFileCreated(@NotNull Path path) {
|
||||
String filename = pathToFilename(path);
|
||||
ImageFile imageFile = new ImageFile(filename, path);
|
||||
if (files.putIfAbsent(filename, imageFile) == null) {
|
||||
LOGGER.fine("Registered file \"" + filename + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On file modified
|
||||
* @param path File path
|
||||
*/
|
||||
protected synchronized void onFileModified(@NotNull Path path) {
|
||||
String filename = pathToFilename(path);
|
||||
ImageFile imageFile = files.get(filename);
|
||||
if (imageFile != null) {
|
||||
imageFile.invalidate();
|
||||
LOGGER.fine("Invalidated file \"" + filename + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On file deleted
|
||||
* @param path File path
|
||||
*/
|
||||
protected synchronized void onFileDeleted(@NotNull Path path) {
|
||||
String filename = pathToFilename(path);
|
||||
ImageFile imageFile = files.remove(filename);
|
||||
if (imageFile != null) {
|
||||
imageFile.invalidate();
|
||||
LOGGER.fine("Unregistered file \"" + filename + "\"");
|
||||
}
|
||||
return cachedImages.get(filename);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package io.josemmo.bukkit.plugin.storage;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* An instance of a file stored in a shared filesystem (<i>e.g.,</i> a NFS drive).
|
||||
* All operations are performed in a blocking way.
|
||||
* That is, the instance will acquire a read or write lock before reading or modifying the file.
|
||||
* If the file is already locked by another resource, it will wait until the lock is released
|
||||
* <b>while blocking the thread</b>.
|
||||
*/
|
||||
public class SynchronizedFile {
|
||||
protected final Path path;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param path Path to file
|
||||
*/
|
||||
public SynchronizedFile(@NotNull Path path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check file exists
|
||||
* @return Whether file exists or not
|
||||
*/
|
||||
public boolean exists() {
|
||||
return Files.exists(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image last modified time
|
||||
* @return Last modified time in milliseconds or <code>0</code> in case of error
|
||||
*/
|
||||
public long getLastModified() {
|
||||
try {
|
||||
return Files.getLastModifiedTime(path).toMillis();
|
||||
} catch (Exception __) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make directories
|
||||
* <p>
|
||||
* Creates the directory containing this file and its parents if they don't exist.
|
||||
*/
|
||||
public void mkdirs() {
|
||||
try {
|
||||
Files.createDirectories(path.getParent());
|
||||
} catch (IOException __) {
|
||||
// Silently ignore exception
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file reader
|
||||
* @return Readable stream of the file
|
||||
* @throws IOException if failed to acquire lock
|
||||
*/
|
||||
public RandomAccessFile read() throws IOException {
|
||||
return new SynchronizedFileStream(path, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file writer
|
||||
* @return Writable stream of the file
|
||||
* @throws IOException if failed to acquire lock
|
||||
*/
|
||||
public RandomAccessFile write() throws IOException {
|
||||
return new SynchronizedFileStream(path, false);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package io.josemmo.bukkit.plugin.storage;
|
||||
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Blocking file stream implementing {@link RandomAccessFile} for reading and writing data.
|
||||
* It will lock the file for reading/writing when opened, and release the lock when closed.
|
||||
*/
|
||||
public class SynchronizedFileStream extends RandomAccessFile {
|
||||
/**
|
||||
* Class constructor
|
||||
* @param path Path to file
|
||||
* @param readOnly Whether to open stream in read-only mode
|
||||
* @throws IOException if failed to acquire lock
|
||||
*/
|
||||
public SynchronizedFileStream(@NotNull Path path, boolean readOnly) throws IOException {
|
||||
super(path.toFile(), readOnly ? "r" : "rw");
|
||||
waitForLock(readOnly);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for lock to be released
|
||||
* @param readOnly Whether to acquire read-only (shared) lock
|
||||
*/
|
||||
@Blocking
|
||||
private void waitForLock(boolean readOnly) throws IOException {
|
||||
FileChannel channel = getChannel();
|
||||
FileLock lock = null;
|
||||
while (lock == null) {
|
||||
try {
|
||||
lock = channel.lock(0L, Long.MAX_VALUE, readOnly);
|
||||
} catch (OverlappingFileLockException e) {
|
||||
try {
|
||||
Thread.sleep(20);
|
||||
} catch (InterruptedException __) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ActionBar {
|
||||
private static final Logger LOGGER = Logger.getLogger("ActionBar");
|
||||
private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private final Player player;
|
||||
private String message;
|
||||
@@ -72,7 +72,7 @@ public class ActionBar {
|
||||
try {
|
||||
ProtocolLibrary.getProtocolManager().sendServerPacket(player, actionBarPacket);
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to send ActionBar to " + player.getName(), e);
|
||||
plugin.log(Level.SEVERE, "Failed to send ActionBar to " + player.getName(), e);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
package io.josemmo.bukkit.plugin.utils;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* A class for parsing simple CSV files.
|
||||
* It's used for loading and writing Yamipa's configuration file ("image.dat" by default) and does not
|
||||
* support escaping of special characters, as this is not currently needed.
|
||||
*/
|
||||
public class CsvConfiguration {
|
||||
public static final Charset CHARSET = StandardCharsets.UTF_8;
|
||||
public static final String COLUMN_DELIMITER = "\t";
|
||||
public static final String COLUMN_DELIMITER = "/";
|
||||
private final List<String[]> data = new ArrayList<>();
|
||||
|
||||
/**
|
||||
@@ -42,25 +38,14 @@ public class CsvConfiguration {
|
||||
* @param path File path
|
||||
* @throws IOException if failed to read file
|
||||
*/
|
||||
public void load(@NotNull Path path) throws IOException {
|
||||
try (Stream<String> stream = Files.lines(path, CHARSET)) {
|
||||
stream.forEach(line -> {
|
||||
line = line.trim();
|
||||
|
||||
// Ignore empty lines
|
||||
if (line.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate legacy format
|
||||
if (!line.contains(COLUMN_DELIMITER)) {
|
||||
line = line.replaceAll("/", COLUMN_DELIMITER);
|
||||
}
|
||||
|
||||
// Parse line
|
||||
public void load(@NotNull String path) throws IOException {
|
||||
Stream<String> stream = Files.lines(Paths.get(path), CHARSET);
|
||||
stream.forEach(line -> {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty()) {
|
||||
addRow(line.split(COLUMN_DELIMITER));
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,8 +53,8 @@ public class CsvConfiguration {
|
||||
* @param path File path
|
||||
* @throws IOException if failed to write file
|
||||
*/
|
||||
public void save(@NotNull Path path) throws IOException {
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(Files.newOutputStream(path), CHARSET)) {
|
||||
public void save(@NotNull String path) throws IOException {
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(path), CHARSET)) {
|
||||
for (String[] row : getRows()) {
|
||||
writer.write(String.join(COLUMN_DELIMITER, row) + "\n");
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public abstract class InteractWithEntityListener implements PacketListener {
|
||||
public static final int MAX_BLOCK_DISTANCE = 5; // Server should only accept entities within a 4-block radius
|
||||
private static final Logger LOGGER = Logger.getLogger("InteractWithEntityListener");
|
||||
|
||||
/**
|
||||
* On player attack listener
|
||||
@@ -91,7 +91,7 @@ public abstract class InteractWithEntityListener implements PacketListener {
|
||||
allowEvent = onInteract(player, targetBlock, targetBlockFace);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to notify entity listener handler", e);
|
||||
YamipaPlugin.getInstance().log(Level.SEVERE, "Failed to notify entity listener handler", e);
|
||||
}
|
||||
|
||||
// Cancel event (if needed)
|
||||
|
||||
@@ -6,7 +6,6 @@ import org.bukkit.Server;
|
||||
import org.bukkit.command.CommandMap;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -14,7 +13,7 @@ public class Internals {
|
||||
public static final float MINECRAFT_VERSION;
|
||||
private static final CommandDispatcher<?> DISPATCHER;
|
||||
private static final CommandMap COMMAND_MAP;
|
||||
private static @Nullable Method GET_BUKKIT_SENDER_METHOD = null;
|
||||
private static final Method GET_BUKKIT_SENDER_METHOD;
|
||||
|
||||
static {
|
||||
try {
|
||||
@@ -31,30 +30,24 @@ public class Internals {
|
||||
Object nmsInstance = obcClass.getDeclaredMethod("getServer").invoke(obcInstance);
|
||||
Class<?> nmsClass = nmsInstance.getClass().getSuperclass();
|
||||
|
||||
if (MINECRAFT_VERSION >= 20.6) {
|
||||
// Get "net.minecraft.commands.Commands" references
|
||||
Object nmsCommandsInstance = nmsClass.getDeclaredMethod("getCommands").invoke(nmsInstance);
|
||||
Class<?> nmsCommandsClass = nmsCommandsInstance.getClass();
|
||||
// Get "net.minecraft.server.CommandDispatcher" references
|
||||
Object nmsDispatcherInstance = nmsClass.getDeclaredField("vanillaCommandDispatcher").get(nmsInstance);
|
||||
Class<?> nmsDispatcherClass = nmsDispatcherInstance.getClass();
|
||||
|
||||
// Get "com.mojang.brigadier.CommandDispatcher" instance
|
||||
Field nmsDispatcherField = nmsCommandsClass.getDeclaredField("dispatcher");
|
||||
nmsDispatcherField.setAccessible(true);
|
||||
DISPATCHER = (CommandDispatcher<?>) nmsDispatcherField.get(nmsCommandsInstance);
|
||||
} else {
|
||||
// Get "net.minecraft.server.CommandDispatcher" references
|
||||
Object nmsDispatcherInstance = nmsClass.getDeclaredField("vanillaCommandDispatcher").get(nmsInstance);
|
||||
Class<?> nmsDispatcherClass = nmsDispatcherInstance.getClass();
|
||||
|
||||
// Get "com.mojang.brigadier.CommandDispatcher" instance
|
||||
Method getDispatcherMethod = nmsDispatcherClass.getDeclaredMethod("a");
|
||||
getDispatcherMethod.setAccessible(true);
|
||||
DISPATCHER = (CommandDispatcher<?>) getDispatcherMethod.invoke(nmsDispatcherInstance);
|
||||
}
|
||||
// Get Brigadier dispatcher instance
|
||||
Method getDispatcherMethod = nmsDispatcherClass.getDeclaredMethod("a");
|
||||
getDispatcherMethod.setAccessible(true);
|
||||
DISPATCHER = (CommandDispatcher<?>) getDispatcherMethod.invoke(nmsDispatcherInstance);
|
||||
|
||||
// Get command map instance
|
||||
Field commandMapField = obcClass.getDeclaredField("commandMap");
|
||||
commandMapField.setAccessible(true);
|
||||
COMMAND_MAP = (CommandMap) commandMapField.get(obcInstance);
|
||||
|
||||
// Get CommandListenerWrapper.getBukkitSender() method
|
||||
Class<?> clwClass = Class.forName(nmsDispatcherClass.getPackage().getName() + ".CommandListenerWrapper");
|
||||
GET_BUKKIT_SENDER_METHOD = clwClass.getDeclaredMethod("getBukkitSender");
|
||||
GET_BUKKIT_SENDER_METHOD.setAccessible(true);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to get internal classes due to incompatible Minecraft server", e);
|
||||
}
|
||||
@@ -83,12 +76,9 @@ public class Internals {
|
||||
*/
|
||||
public static @NotNull CommandSender getBukkitSender(@NotNull Object source) {
|
||||
try {
|
||||
if (GET_BUKKIT_SENDER_METHOD == null) {
|
||||
GET_BUKKIT_SENDER_METHOD = source.getClass().getDeclaredMethod("getBukkitSender");
|
||||
}
|
||||
return (CommandSender) GET_BUKKIT_SENDER_METHOD.invoke(source);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to extract Bukkit sender from source", e);
|
||||
throw new RuntimeException("Failed to extract bukkit sender from source", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package io.josemmo.bukkit.plugin.utils;
|
||||
|
||||
import io.josemmo.bukkit.plugin.YamipaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Custom logging class used by components from the plugin.
|
||||
* It is <i>instance agnostic</i>, that is, the same logger instance will work regardless of whether the plugin
|
||||
* instance has changed (for example, because of the plugin being restarted by a plugin manager).
|
||||
*/
|
||||
public class Logger {
|
||||
private final @Nullable String name;
|
||||
|
||||
/**
|
||||
* Get logger instance
|
||||
* @param name Logger name
|
||||
* @return Logger instance
|
||||
*/
|
||||
public static @NotNull Logger getLogger(@NotNull String name) {
|
||||
return new Logger(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logger instance
|
||||
* @return Logger instance
|
||||
*/
|
||||
public static @NotNull Logger getLogger() {
|
||||
return new Logger(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
* @param name Optional logger name
|
||||
*/
|
||||
private Logger(@Nullable String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log message
|
||||
* @param level Record level
|
||||
* @param message Message
|
||||
* @param e Optional throwable to log
|
||||
*/
|
||||
private void log(@NotNull Level level, @NotNull String message, @Nullable Throwable e) {
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
|
||||
// Handle verbose logging levels
|
||||
if (level.intValue() < Level.INFO.intValue()) {
|
||||
if (!plugin.isVerbose()) return;
|
||||
level = Level.INFO;
|
||||
}
|
||||
|
||||
// Add logger name to message
|
||||
if (name != null) {
|
||||
message = "[" + name + "] " + message;
|
||||
}
|
||||
|
||||
// Proxy record to real logger
|
||||
if (e == null) {
|
||||
plugin.getLogger().log(level, message);
|
||||
} else {
|
||||
plugin.getLogger().log(level, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log severe message
|
||||
* @param message Message
|
||||
* @param e Throwable to log
|
||||
*/
|
||||
public void severe(@NotNull String message, @NotNull Throwable e) {
|
||||
log(Level.SEVERE, message, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
* @param message Message
|
||||
* @param e Throwable to log
|
||||
*/
|
||||
public void warning(@NotNull String message, @NotNull Throwable e) {
|
||||
log(Level.WARNING, message, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
* @param message Message
|
||||
*/
|
||||
public void warning(@NotNull String message) {
|
||||
log(Level.WARNING, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
* @param message Message
|
||||
*/
|
||||
public void info(@NotNull String message) {
|
||||
log(Level.INFO, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log fine message
|
||||
* @param message Message
|
||||
*/
|
||||
public void fine(@NotNull String message) {
|
||||
log(Level.FINE, message, null);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,6 @@ import me.angeschossen.lands.api.flags.type.RoleFlag;
|
||||
import me.angeschossen.lands.api.land.LandWorld;
|
||||
import me.angeschossen.lands.api.player.LandPlayer;
|
||||
import me.ryanhamshire.GriefPrevention.GriefPrevention;
|
||||
import net.luckperms.api.LuckPerms;
|
||||
import net.luckperms.api.LuckPermsProvider;
|
||||
import org.anjocaido.groupmanager.GroupManager;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
@@ -27,29 +24,15 @@ import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Permissions {
|
||||
private static final Logger LOGGER = Logger.getLogger();
|
||||
private static @Nullable LuckPerms luckPerms;
|
||||
private static @Nullable GroupManager groupManager;
|
||||
private static @Nullable WorldGuard worldGuard;
|
||||
private static @Nullable GriefPrevention griefPrevention;
|
||||
private static @Nullable TownyAPI townyApi;
|
||||
private static @Nullable LandsIntegration landsApi;
|
||||
@Nullable private static WorldGuard worldGuard = null;
|
||||
@Nullable private static GriefPrevention griefPrevention = null;
|
||||
@Nullable private static TownyAPI townyApi = null;
|
||||
@Nullable private static LandsIntegration landsApi = null;
|
||||
|
||||
static {
|
||||
try {
|
||||
luckPerms = LuckPermsProvider.get();
|
||||
} catch (NoClassDefFoundError | IllegalStateException __) {
|
||||
// LuckPerms is not installed
|
||||
}
|
||||
|
||||
try {
|
||||
groupManager = (GroupManager) YamipaPlugin.getInstance().getServer().getPluginManager().getPlugin("GroupManager");
|
||||
} catch (NoClassDefFoundError __) {
|
||||
// GroupManager is not installed
|
||||
}
|
||||
|
||||
try {
|
||||
worldGuard = WorldGuard.getInstance();
|
||||
} catch (NoClassDefFoundError __) {
|
||||
@@ -75,27 +58,6 @@ public class Permissions {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get player variable
|
||||
* @param variable Variable name (key)
|
||||
* @param player Player instance
|
||||
* @return Variable value or NULL if not found
|
||||
*/
|
||||
public static @Nullable String getVariable(@NotNull String variable, @NotNull Player player) {
|
||||
if (luckPerms != null) {
|
||||
return luckPerms.getPlayerAdapter(Player.class).getUser(player).getCachedData().getMetaData()
|
||||
.getMetaValue(variable);
|
||||
}
|
||||
|
||||
if (groupManager != null) {
|
||||
String rawValue = groupManager.getWorldsHolder().getWorldPermissions(player)
|
||||
.getPermissionString(player.getName(), variable);
|
||||
return rawValue.isEmpty() ? null : rawValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can build at this block
|
||||
* @param player Player instance
|
||||
@@ -162,7 +124,7 @@ public class Permissions {
|
||||
canEditCallable.call() :
|
||||
Bukkit.getScheduler().callSyncMethod(plugin, canEditCallable).get();
|
||||
} catch (Exception e) {
|
||||
LOGGER.severe("Failed to get player permissions from GriefPrevention", e);
|
||||
plugin.log(Level.SEVERE, "Failed to get player permissions from GriefPrevention", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import java.util.UUID;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
public class SelectBlockTask {
|
||||
private static final Logger LOGGER = Logger.getLogger("SelectBlockTask");
|
||||
private static final YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
private static final Map<UUID, SelectBlockTask> instances = new HashMap<>();
|
||||
private static SelectBlockTaskListener listener = null;
|
||||
private final Player player;
|
||||
@@ -71,7 +71,7 @@ public class SelectBlockTask {
|
||||
if (listener == null) {
|
||||
listener = new SelectBlockTaskListener();
|
||||
listener.register();
|
||||
LOGGER.fine("Created SelectBlockTaskListener singleton");
|
||||
plugin.fine("Created SelectBlockTaskListener singleton");
|
||||
}
|
||||
|
||||
// Start task
|
||||
@@ -94,7 +94,7 @@ public class SelectBlockTask {
|
||||
if (instances.isEmpty()) {
|
||||
listener.unregister();
|
||||
listener = null;
|
||||
LOGGER.fine("Destroyed SelectBlockTaskListener singleton");
|
||||
plugin.fine("Destroyed SelectBlockTaskListener singleton");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,6 @@ public class SelectBlockTask {
|
||||
@Override
|
||||
public void register() {
|
||||
super.register();
|
||||
YamipaPlugin plugin = YamipaPlugin.getInstance();
|
||||
plugin.getServer().getPluginManager().registerEvents(this, plugin);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,10 @@ main: io.josemmo.bukkit.plugin.YamipaPlugin
|
||||
api-version: 1.16
|
||||
depend: [ProtocolLib]
|
||||
softdepend:
|
||||
- BentoBox
|
||||
- GriefPrevention
|
||||
- GroupManager
|
||||
- Hyperverse
|
||||
- LuckPerms
|
||||
- Multiverse-Core
|
||||
- My_Worlds
|
||||
- PhantomWorlds
|
||||
- Towny
|
||||
- WorldGuard
|
||||
|
||||
|
||||
Reference in new issue
Block a user