Compare commits

...
62 Commits
Author SHA1 Message Date
NotAlexNoyle 4c8332906a Point submodules at canonical Gitea repositories 2026-11-01 12:49:27 -05:00
NotAlexNoyle b0acf41a11 Update canonical Gitea link 2026-10-30 14:20:05 -04:00
NotAlexNoyle 8e84836e75 Exclude CODEOWNERS from language statistics 2026-10-29 16:19:06 -04:00
NotAlexNoyle 68dbd9406d Add CODEOWNERS (one staff approval) 2026-10-29 16:19:06 -04:00
NotAlexNoyle c5720d02f6 Merge pull request #3 from true-og/codex/change-/images-command-to-/yamipa-and-fix-registration
Register /yamipa command and resync Brigadier (Purpur 1.19.4)
2026-03-21 13:12:50 -04:00
NotAlexNoyle 433d28e7fd Fix yamipa command registration 2026-03-18 13:42:47 -04:00
NotAlexNoyle 65cab28e16 Update README.md 2026-03-18 11:51:08 -04:00
NotAlexNoyle f8a1ee792f Finish Utilities-OG migration, resolve compile time warnings 2026-03-18 11:50:19 -04:00
NotAlexNoyle 23deb1c7c1 Merge pull request #2 from true-og/codex/migrate-to-bukkit-color-codes-and-utilitiesog-api
Migrate messaging to UtilitiesOG &-code formatting
2026-03-18 11:36:55 -04:00
NotAlexNoyle 95ee414738 Migrate legacy chat formatting to UtilitiesOG 2026-03-18 11:30:24 -04:00
NotAlexNoyle 79c4177af7 Begin Utilities-OG migration from ChatColor 2026-03-18 11:24:06 -04:00
NotAlexNoyle 6804f900ef Delete automata 2026-03-18 11:05:51 -04:00
NotAlexNoyle 4cc64a8864 Merge pull request #1 from true-og/codex/change-plugin-name-to-yamipa-og
Rename plugin to Yamipa-OG and produce installable JAR as build/libs/Yamipa-$version-.jar
2026-03-18 10:48:54 -04:00
NotAlexNoyle 0a42960a4b Rename plugin to Yamipa-OG 2026-03-18 10:47:42 -04:00
NotAlexNoyle 39909a94ef Add Utilities-OG API 2026-03-18 10:37:50 -04:00
NotAlexNoyle 6ca1283c63 Remove unnecessary dependencies 2025-12-07 18:21:18 -05:00
NotAlexNoyle 06d854d0b8 Maven -> Gradle 8.14.3 2025-08-31 10:31:44 -04:00
José Miguel Moreno b5b28e1372 Merge pull request #155 from josemmo/develop
v1.3.3
2025-08-09 10:12:09 +02:00
José Miguel Moreno 3153ae3383 v1.3.3 2025-08-09 10:10:11 +02:00
José Miguel Moreno 4645b8f601 Updated ProtocolLib repository 2025-08-08 19:40:23 +02:00
José Miguel Moreno a7adb984e9 Updated tests workflow
> Builds for Bukkit/Spigot 1.17.1 are missing, we hardcode the URLs for those exceptions.
2025-07-29 22:07:22 +02:00
José Miguel Moreno 548ed82ff4 Upgraded dependencies 2025-07-29 21:02:51 +02:00
José Miguel Moreno c78a51ee07 Removed unused ProtocolLib initialization
- Updated FakeEntity class
- Updated FakeImage class
- Updated YamipaPlugin class
2025-07-29 20:57:28 +02:00
José Miguel Moreno 641e7532bf Added support for Minecraft 1.21.8 2025-07-29 20:44:57 +02:00
José Miguel Moreno 03a5a4ab91 Lowered ProtocolLib version requirement
- Updated README.md
- Updated tests.yml workflow
2025-07-29 20:43:58 +02:00
José Miguel Moreno b20ca80350 Merge pull request #153 from MusclePr/develop
WrappedDataWatcher may require lazy initialization as of ProtocolLib 5.3.0.
2025-07-29 19:18:40 +02:00
José Miguel Moreno 566d0b2f18 Added data watcher annotations to EntityMetadataPacket
- Updated EntityMetadataPacket class
2025-07-29 19:14:14 +02:00
MusclePr 9b44062fac WrappedDataWatcher may require lazy initialization as of ProtocolLib 5.3.0. 2025-07-28 17:42:53 +09:00
José Miguel Moreno 80d184a9a3 v1.3.3-SNAPSHOT 2025-07-09 19:58:43 +02:00
José Miguel Moreno af873a7d7f Fixed download URLs in tests workflow (IV)
- Updated tests.yml
2025-07-09 19:56:29 +02:00
José Miguel Moreno aa1f4d9321 Fixed download URLs in tests workflow (III) 2025-07-08 23:13:10 +02:00
José Miguel Moreno f487493b73 Fixed download URLs in tests workflow (II) 2025-07-07 22:59:40 +02:00
José Miguel Moreno ed97bf7bb6 Fixed download URLs in tests workflow
- Updated tests.yml
2025-07-07 22:56:21 +02:00
José Miguel Moreno 004accd011 Upgraded automata 2025-07-07 20:59:09 +02:00
José Miguel Moreno f4dca338fe Upgraded tests workflow
- Updated tests.yml
2025-07-07 20:58:56 +02:00
José Miguel Moreno a5f5ffe29a Fixed ProtocolLib deprecation
- Updated EntityMetadataPacket class
- Updated FakeEntity class
2025-07-07 20:58:36 +02:00
José Miguel Morenoandまっする de11efac71 Added support for Minecraft 1.21.7
- Updated EntityMetadataPacket class
- Updated README.md

> Closes #149

Co-Authored-By: まっする <158264076+musclepr@users.noreply.github.com>
2025-07-07 20:57:46 +02:00
José Miguel Moreno 45a9e126fc Upgraded dependencies
- Updated pom.xml
2025-07-07 20:43:15 +02:00
José Miguel Moreno 655cf82b50 Merge pull request #140 from josemmo/develop
v1.3.2
2024-10-13 12:24:49 +02:00
José Miguel Moreno a9ff08fb30 v1.3.2 2024-10-13 12:20:03 +02:00
José Miguel Moreno 9933b43254 Added 1.21.x to tests
- Updated tests.yml workflow
- Updated README.md
2024-10-13 12:07:06 +02:00
José Miguel Moreno 4ed37c6a9d Upgraded automata 2024-10-13 11:57:52 +02:00
José Miguel Moreno 055ecedf22 Merge pull request #137 from Ingrim4/feat/support-1.21
Add support for 1.20.6/1.21
2024-10-13 11:46:46 +02:00
José Miguel Moreno 955c7c2a1f Upgraded dependencies 2024-10-13 11:38:39 +02:00
José Miguel Moreno c6ff5bb877 Minor code style changes
- Updated FakeEntity and FakeImage classes
- Updated Internals class
2024-10-13 11:28:08 +02:00
José Miguel Moreno 25d5a33d86 Simplified MapDataPacket
- Updated MapDataPacket class
- Removed WrappedMapId class
2024-10-13 11:22:31 +02:00
Maurice Eisenblätter f322213986 fix: add support for vanilla class mappings 2024-10-06 17:37:05 +02:00
Maurice Eisenblätter ffb7d97b1d feat: add support for 1.21 2024-10-06 15:38:11 +02:00
José Miguel Moreno 0e678dd0e0 Switched filled map item stack instantiation method
- Updated FakeItemFrame class

> Suggested by @Ingrim4 in #125
2024-10-05 14:01:41 +02:00
José Miguel Moreno 25ef6fad6b Migrated to network injection handler
- Updated FakeEntity class
2024-10-05 13:51:53 +02:00
José Miguel Moreno ab16089659 Upgraded ProtocolLib dependency
- Updated pom.xml
2024-10-05 13:51:08 +02:00
José Miguel Moreno 3f5c99094d Fixed GriefPrevention dependency
- Updated pom.xml
2024-10-05 13:49:00 +02:00
José Miguel Moreno c15650847e Minor lint change
- Updated EntityMetadataPacket class
- Updated FakeEntity class
2024-09-28 18:05:23 +02:00
José Miguel Moreno a7770d9956 Minor version fix in command dispatcher
- Updated Internals class
2024-09-28 17:57:02 +02:00
José Miguel Moreno a51c7777a2 Merge pull request #117 from josemmo/develop
v1.3.1
2024-03-02 12:41:44 +01:00
José Miguel Moreno 17c59ea00e Merge pull request #112 from josemmo/develop
v1.3.0
2024-01-06 12:08:41 +01:00
José Miguel Moreno 347ab60b88 Merge pull request #111 from josemmo/develop
v1.2.13
2023-12-26 11:17:01 +01:00
José Miguel Moreno 076d999005 Merge pull request #106 from josemmo/develop
v1.2.12
2023-10-14 14:43:17 +02:00
José Miguel Moreno d421ff2a85 Merge pull request #97 from josemmo/develop
v1.2.11
2023-06-17 12:31:33 +02:00
José Miguel Moreno 494e18b128 Merge pull request #94 from josemmo/develop
v1.2.10
2023-05-07 11:49:25 +02:00
José Miguel Moreno a786471adb Merge pull request #86 from josemmo/develop
v1.2.9
2023-03-25 12:19:06 +01:00
José Miguel Moreno 2d09397800 Merge pull request #81 from josemmo/develop
v1.2.8
2023-02-02 19:45:53 +01:00
68 changed files with 4687 additions and 4769 deletions

No files matched your search

+1
View File
@@ -1 +1,2 @@
* text=auto eol=lf
.gitea/CODEOWNERS linguist-detectable=false
+3
View File
@@ -0,0 +1,3 @@
# Pull requests request review from the eligible staff team(s).
# One approval from either an Admin or Developer satisfies branch protection.
* @trueog/Admins @trueog/Developers
-30
View File
@@ -1,30 +0,0 @@
name: Build
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
# Download code from repository
- name: Checkout code
uses: actions/checkout@v4
# Setup Java
- name: Setup JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
# Build plugin
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
# Upload release
- name: Upload release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh release upload ${GITHUB_REF##*/} ./target/YamipaPlugin-*.jar
-138
View File
@@ -1,138 +0,0 @@
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 8
continue-on-error: ${{ matrix.version == '1.20.4' }}
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']
steps:
# Download code from repository
- name: Checkout code
uses: actions/checkout@v4
# Setup Java (for building)
- name: Setup JDK (for building)
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
# Build plugin
- name: Build with Maven
run: mvn --batch-mode --update-snapshots verify
# Setup Java (for testing)
- name: Setup JDK (for testing)
uses: actions/setup-java@v4
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'
}}
# Setup Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
# Setup automata
- name: Setup automata
working-directory: ./automata
run: npm install
# Setup server
- name: Setup server
env:
FLAVOR: ${{ matrix.flavor }}
VERSION: ${{ matrix.version }}
GETBUKKIT_SUBDOMAIN: ${{ startsWith(matrix.version, '1.16') && 'cdn' || 'download' }}
run: |
if [ $FLAVOR == "bukkit" ]; then
url="https://$GETBUKKIT_SUBDOMAIN.getbukkit.org/craftbukkit/craftbukkit-$VERSION.jar"
elif [ $FLAVOR == "spigot" ]; then
url="https://$GETBUKKIT_SUBDOMAIN.getbukkit.org/spigot/spigot-$VERSION.jar"
elif [ $FLAVOR == "paper" ]; then
build=$(curl "https://papermc.io/api/v2/projects/paper/versions/$VERSION" | jq '.builds[-1]')
url="https://papermc.io/api/v2/projects/paper/versions/$VERSION/builds/$build/downloads/paper-$VERSION-$build.jar"
elif [ $FLAVOR == "purpur" ]; then
url="https://api.purpurmc.org/v2/purpur/$VERSION/latest/download"
fi
mkdir ./server && cd ./server
wget -nv "$url" -O server.jar
java -jar server.jar || true
sed -i 's/eula=false/eula=true/g' eula.txt
echo 'online-mode=false' >> server.properties
echo 'enable-rcon=true' >> server.properties
echo 'rcon.password=rcon' >> server.properties
echo 'allow-nether=false' >> server.properties
echo 'allow-end=false' >> server.properties
echo 'generate-structures=false' >> server.properties
echo 'level-type=flat' >> server.properties
# Install dependencies
- name: Install plugin dependencies
run: |
url="https://ci.dmulloy2.net/job/ProtocolLib/lastSuccessfulBuild/artifact/build/libs/ProtocolLib.jar"
mkdir -p ./server/plugins/ProtocolLib
wget -q "$url" -O ./server/plugins/ProtocolLib.jar
echo -e "global:\n auto updater:\n notify: false\n download: false" > ./server/plugins/ProtocolLib/config.yml
# Install plugin
- name: Install plugin
run: |
mkdir -p ./server/plugins/YamipaPlugin/images
cp ./target/YamipaPlugin-*.jar ./server/plugins/
echo "verbose: true" > ./server/plugins/YamipaPlugin/config.yml
for i in {1..5}; do
wget -q "https://picsum.photos/400" -O "./server/plugins/YamipaPlugin/images/pic-$i.jpg"
done
# Test plugin
- name: Run plugin in server
working-directory: ./server
env:
FLAVOR: ${{ matrix.flavor }}
run: |
(cd ../automata && npm run start) &
timeout -s SIGINT -k 110s 90s java -DIReallyKnowWhatIAmDoingISwear -jar server.jar nogui | tee server.log || true
if ! grep -Fq '[YamipaPlugin] ' server.log; then
echo "Plugin did not load"
exit 1
fi
if ! grep -Fq '[YamipaPlugin] Running on VERBOSE mode' server.log; then
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
echo "Plugin did not read image directory"
exit 1
fi
if ! grep -Fq '[YamipaPlugin] [ImageCommandBridge] 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
echo "Plugin did not place the fake image"
exit 1
fi
if ! grep -Fq '[YamipaPlugin] [FakeImage] Invalidated FakeImage' server.log; then
echo "Plugin did not remove the fake image"
exit 1
fi
if [ "$FLAVOR" != "bukkit" ] && grep -iFq 'exception' server.log; then
echo "Server threw an exception"
exit 1
fi
+6 -3
View File
@@ -1,4 +1,7 @@
/.idea
/target
/*.iml
.gradle/
build/
bin/
.project
.classpath
.settings/
node_modules
+3
View File
@@ -0,0 +1,3 @@
[submodule "libs/Utilities-OG"]
path = libs/Utilities-OG
url = https://git.trueog.net/trueog/Utilities-OG
+4 -1
View File
@@ -5,6 +5,8 @@
[![bStats Servers](https://img.shields.io/bstats/servers/10243)](https://bstats.org/plugin/bukkit/Yamipa/10243)
[![License](https://img.shields.io/github/license/josemmo/yamipa)](LICENSE)
Yamipa-OG is a fork of Yamipa maintained by [TrueOG Network](https://git.trueog.net/trueog/Yamipa-OG) for Purpur 1.19.4.
Yamipa is a Spigot plugin that allows players to place images (even **animated**!) on any surface in your Minecraft server
without having to install any local client mod.
@@ -25,7 +27,7 @@ Download the JAR file for the [latest release](https://github.com/josemmo/yamipa
Before installing Yamipa make sure you meet the following requirements:
- CraftBukkit, Spigot or PaperMC 1.16 or higher
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) v5.0.0 or higher
- [ProtocolLib](https://www.spigotmc.org/resources/protocollib.1997/) v5.3.0 or higher
Here are the Minecraft distributions where Yamipa should be able to run:
| Minecraft version | CraftBukkit | Spigot | PaperMC |
@@ -35,6 +37,7 @@ Here are the Minecraft distributions where Yamipa should be able to run:
| 1.18 & 1.18.x | ✅ | ✅ | ✅ |
| 1.19 & 1.19.x | ✅ | ✅ | ✅ |
| 1.20 & 1.20.x | ✅ | ✅ | ✅ |
| 1.21 & 1.21.x | ✅ | ✅ | ✅ |
## Configuration
Yamipa is ready-to-go right out of the box. By default, it creates the following files and directories under the
-45
View File
@@ -1,45 +0,0 @@
import { placeBlockOnTheFloor, startBot } from './src/bot.js'
import { wait } from './src/common.js'
import { getRconClient, waitForServer } from './src/rcon.js'
(async() => {
console.log('[AUTOMATA] Waiting for Minecraft server to be ready...')
await waitForServer()
console.log('[AUTOMATA] Logging in as test account...')
const bot = await startBot()
await bot.look(0, -Math.PI/2)
console.log('[AUTOMATA] Making test account an OP...')
const conn = await getRconClient()
await conn.send('op test')
await conn.send('gamemode creative test')
await wait(2000)
console.log('[AUTOMATA] Preparing test account...')
bot.chat('/clear @p')
await wait(2000)
bot.chat('/give @p minecraft:dirt')
await wait(2000)
console.log('[AUTOMATA] Placing image on the floor...')
bot.chat('/image place pic-1.jpg 4 4')
await wait(2000)
placeBlockOnTheFloor(bot)
await wait(2000)
console.log('[AUTOMATA] Removing image from floor...')
bot.chat('/image remove')
await wait(2000)
placeBlockOnTheFloor(bot)
await wait(2000)
console.log('[AUTOMATA] Logging off...')
bot.end()
console.log('[AUTOMATA] Stopping server...')
await conn.send('stop')
await conn.end()
console.log('[AUTOMATA] Done!')
})()
-1672
View File
File diff suppressed because it is too large. Load diff
-12
View File
@@ -1,12 +0,0 @@
{
"name": "@yamipa/automata",
"description": "Minecraft Automata for testing Yamipa",
"type": "module",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"mineflayer": "^4.20.1",
"rcon-client": "^4.2.4"
}
}
-28
View File
@@ -1,28 +0,0 @@
import mineflayer from 'mineflayer'
/**
* @return {Promise<mineflayer.Bot>} Bot instance
*/
export function startBot() {
return new Promise(resolve => {
const bot = mineflayer.createBot({
host: '127.0.0.1',
username: 'test'
})
bot.once('spawn', () => resolve(bot))
})
}
/**
* @param {mineflayer.Bot} bot Bot instance
*/
export function placeBlockOnTheFloor(bot) {
bot._client.write('block_place', {
location: bot.entity.position.offset(0, -1, 0),
direction: 1,
hand: 0,
cursorX: 0,
cursorY: 0,
cursorZ: 0
})
}
-3
View File
@@ -1,3 +0,0 @@
export function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
-26
View File
@@ -1,26 +0,0 @@
import { Rcon } from 'rcon-client'
import { wait } from './common.js'
const CONNECTION = {
host: '127.0.0.1',
port: 25575,
password: 'rcon',
timeout: 1000*20
}
export async function waitForServer() {
while (true) {
try {
const rcon = await Rcon.connect(CONNECTION)
await rcon.end()
break
} catch (_) {
// Not ready yet
}
wait(2000)
}
}
export function getRconClient() {
return Rcon.connect(CONNECTION)
}
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/bash
# Fetch all submodule content.
git submodule update --force --recursive --init
+161
View File
@@ -0,0 +1,161 @@
/* This is free and unencumbered software released into the public domain */
import org.gradle.kotlin.dsl.provideDelegate
/* ------------------------------ Plugins ------------------------------ */
plugins {
id("java") // Import Java plugin.
id("java-library") // Import Java Library plugin.
id("com.diffplug.spotless") version "8.1.0" // Import Spotless plugin.
id("com.gradleup.shadow") version "8.3.9" // Import Shadow plugin.
id("checkstyle") // Import Checkstyle plugin.
eclipse // Import Eclipse plugin.
kotlin("jvm") version "2.1.21" // Import Kotlin JVM plugin.
}
extra["kotlinAttribute"] = Attribute.of("kotlin-tag", Boolean::class.javaObjectType)
val kotlinAttribute: Attribute<Boolean> by rootProject.extra
/* --------------------------- JDK / Kotlin ---------------------------- */
java {
sourceCompatibility = JavaVersion.VERSION_17 // Compile with JDK 17 compatibility.
toolchain { // Select Java toolchain.
languageVersion.set(JavaLanguageVersion.of(17)) // Use JDK 17.
vendor.set(JvmVendorSpec.GRAAL_VM) // Use GraalVM CE.
}
}
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
vendor.set(JvmVendorSpec.GRAAL_VM)
}
}
/* ----------------------------- Metadata ------------------------------ */
group = "io.josemmo.bukkit.plugin" // Declare bundle identifier.
version = "1.3.3" // Declare plugin version (will be in .jar).
val apiVersion = "1.19" // Declare minecraft server target version.
/* ----------------------------- Resources ----------------------------- */
tasks.named<ProcessResources>("processResources") {
val props = mapOf("version" to version, "apiVersion" to apiVersion)
inputs.properties(props) // Indicates to rerun if version changes.
filesMatching("plugin.yml") { expand(props) }
from("LICENSE") { into("/") } // Bundle licenses into jarfiles.
}
/* ---------------------------- Repos ---------------------------------- */
repositories {
mavenCentral() // Import the Maven Central Maven Repository.
gradlePluginPortal() // Import the Gradle Plugin Portal Maven Repository.
maven { url = uri("https://repo.purpurmc.org/snapshots") } // Import the PurpurMC Maven Repository.
maven { url = uri("https://hub.spigotmc.org/nexus/content/groups/public/") }
maven { url = uri("https://libraries.minecraft.net") }
maven { url = uri("https://maven.enginehub.org/repo/") }
maven { url = uri("https://jitpack.io") }
maven { url = uri("file://${System.getProperty("user.home")}/.m2/repository") }
System.getProperty("SELF_MAVEN_LOCAL_REPO")?.let { // TrueOG Bootstrap mavenLocal().
val dir = file(it)
if (dir.isDirectory) {
println("Using SELF_MAVEN_LOCAL_REPO at: $it")
maven { url = uri("file://${dir.absolutePath}") }
} else {
logger.error("TrueOG Bootstrap not found, defaulting to ~/.m2 for mavenLocal()")
mavenLocal()
}
} ?: logger.error("TrueOG Bootstrap not found, defaulting to ~/.m2 for mavenLocal()")
}
/* ---------------------- Java project deps ---------------------------- */
dependencies {
compileOnly("org.purpurmc.purpur:purpur-api:1.19.4-R0.1-SNAPSHOT") // Declare Purpur API version to be packaged.
compileOnlyApi(project(":libs:Utilities-OG")) // Import TrueOG Network Utilities-OG Java API (from source).
compileOnly(files("libs/ProtocolLib-5.0.jar")) // Import Legacy ProtocolLib API.
compileOnly("com.mojang:brigadier:1.0.18")
compileOnly("net.luckperms:api:5.5")
compileOnly("com.github.ElgarL:groupmanager:3.2")
compileOnly("com.sk89q.worldedit:worldedit-bukkit:7.3.0-SNAPSHOT") // Import WorldEdit API.
compileOnly(
"com.sk89q.worldguard:worldguard-bukkit:7.0.9"
) { // Import WorldGuard but without its bundled WorldEdit.
exclude(group = "com.sk89q.worldedit")
}
compileOnly("com.github.GriefPrevention:GriefPrevention:16.18.4")
compileOnly("com.github.TownyAdvanced.Towny:towny:0.101.2.0")
compileOnly("com.github.Angeschossen:LandsAPI:7.15.20")
compileOnly("org.jetbrains:annotations:26.0.2")
}
apply(from = "eclipse.gradle.kts") // Import eclipse classpath support script.
/* ---------------------- Reproducible jars ---------------------------- */
tasks.withType<AbstractArchiveTask>().configureEach { // Ensure reproducible .jars
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}
/* ----------------------------- Shadow -------------------------------- */
tasks.shadowJar {
exclude("io.github.miniplaceholders.*") // Exclude the MiniPlaceholders package from being shadowed.
exclude("META-INF/**")
exclude("LICENSE")
archiveClassifier.set("") // Use empty string instead of null.
archiveFileName.set("Yamipa-OG-${project.version}.jar")
}
tasks.jar {
archiveClassifier.set("part") // Applies to root jarfile only.
archiveFileName.set("Yamipa-OG-${project.version}-part.jar")
}
tasks.build { dependsOn(tasks.spotlessApply, tasks.shadowJar) } // Build depends on spotless and shadow.
/* --------------------------- Javac opts ------------------------------- */
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("-parameters") // Enable reflection for java code.
options.isFork = true // Run javac in its own process.
options.compilerArgs.add("-Xlint:deprecation") // Trigger deprecation warning messages.
options.encoding = "UTF-8" // Use UTF-8 file encoding.
}
/* ----------------------------- Auto Formatting ------------------------ */
spotless {
java {
eclipse().configFile("config/formatter/eclipse-java-formatter.xml") // Eclipse java formatting.
leadingTabsToSpaces() // Convert leftover leading tabs to spaces.
removeUnusedImports() // Remove imports that aren't being called.
}
kotlinGradle {
ktfmt().kotlinlangStyle().configure { it.setMaxWidth(120) } // JetBrains Kotlin formatting.
target("build.gradle.kts", "settings.gradle.kts") // Gradle files to format.
}
}
checkstyle {
toolVersion = "10.18.1" // Declare checkstyle version to use.
configFile = file("config/checkstyle/checkstyle.xml") // Point checkstyle to config file.
isIgnoreFailures = true // Don't fail the build if checkstyle does not pass.
isShowViolations = true // Show the violations in any IDE with the checkstyle plugin.
}
tasks.named("compileJava") {
dependsOn("spotlessApply") // Run spotless before compiling with the JDK.
}
tasks.named("spotlessCheck") {
dependsOn("spotlessApply") // Run spotless before checking if spotless ran.
}
/* ------------------------------ Eclipse SHIM ------------------------- */
// This can't be put in eclipse.gradle.kts because Gradle is weird.
subprojects {
apply(plugin = "java-library")
apply(plugin = "eclipse")
eclipse.project.name = "${project.name}-${rootProject.name}"
tasks.withType<Jar>().configureEach { archiveBaseName.set("${project.name}-${rootProject.name}") }
}
+34
View File
@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="FileTabCharacter">
<property name="eachLine" value="true"/>
<property name="severity" value="error"/>
</module>
<module name="TreeWalker">
<module name="FinalLocalVariable"/>
<module name="AvoidStarImport"/>
<module name="Indentation">
<property name="severity" value="ignore"/>
</module>
</module>
<module name="RegexpSingleline">
<property name="format" value="\borg\.junit\.jupiter\.api\.Assertions\.fail\s*\(\s*&quot;Not yet implemented&quot;\s*\)\s*;"/>
<property name="message" value="Remove placeholder fail() and implement the test."/>
<property name="ignoreCase" value="true"/>
<property name="severity" value="warning"/>
</module>
<module name="RegexpSingleline">
<property name="format" value="(?&lt;!\w)fail\s*\(\s*&quot;Not yet implemented&quot;\s*\)\s*;"/>
<property name="message" value="Remove placeholder fail() and implement the test."/>
<property name="severity" value="warning"/>
</module>
</module>
+404
View File
@@ -0,0 +1,404 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<profiles version="23">
<profile kind="CodeFormatterProfile" name="TrueOG Network Formatting" version="23">
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_for_statment" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_enum_constant_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.align_with_spaces" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_before_code_block" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_switch_case_expressions" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<setting id="org.eclipse.jdt.core.formatter.comment.count_line_length_from_starting_position" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_record_components" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_multiplicative_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameterized_type_references" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_logical_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_annotation_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_multiplicative_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_abstract_method" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.keep_enum_constant_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_catch_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiplicative_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.keep_anonymous_type_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_switch_case_expressions" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_shift_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_code_block" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_bitwise_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_parameters" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_loops" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_for_body_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_switch_case_arrow_operator" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_annotation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.text_block_indentation" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_module_statements" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.align_tags_names_descriptions" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_if_then_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_permitted_types" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression_chain" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_annotations" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines" value="2147483647"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_not_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_type_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_delcaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_type_arguments" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_permitted_types_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_record_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.javadoc_do_not_separate_block_tags" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_tag_description" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_constructor" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_string_concatenation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_shift_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_shift_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_do_while_body_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_record_components" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_for_loop_header" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_additive_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_getter_setter_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case_after_arrow" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_string_concatenation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_lambda_arrow" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_relational_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_logical_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_method_invocation" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_record_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_switch_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_arrow_in_switch_default" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_end_of_method_body" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_if_while_statement" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.keep_switch_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_arrow" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.align_tags_descriptions_grouped" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_method_body_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_loop_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_type_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_additive_operator" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_constructor" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_relational_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_record_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_lambda_body" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_relational_operator" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.align_arrows_in_switch_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_additive_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_string_concatenation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.align_selector_in_method_invocation_on_expression_first_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_record_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_switch_case_with_arrow_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_switch_case_with_colon" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_after_code_block" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_arrow_in_switch_default" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_between_different_tags" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_additive_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_conditional_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_line_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_shift_operator" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_try_clause" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.keep_code_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_record_components" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="4"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_bitwise_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_record_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_assignment_operator" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_switch_case_with_arrow" value="20"/>
<setting id="org.eclipse.jdt.core.formatter.keep_lambda_body_block_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method" value="49"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_record_constructor_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_record_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assertion_message" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_logical_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_relational_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_last_class_body_declaration" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.keep_simple_while_body_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_logical_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_statement_group_in_switch" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.parentheses_positions_in_lambda_declaration" value="common_lines"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_permitted_types" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_enum_declaration_on_one_line" value="one_line_never"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="next_line_on_wrap"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_multiplicative_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_code_block" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_string_concatenation" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="120"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
</profile>
</profiles>
+151
View File
@@ -0,0 +1,151 @@
/* This is free and unencumbered software released into the public domain */
import org.gradle.plugins.ide.eclipse.model.EclipseModel
import java.nio.file.Files
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.OutputKeys
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
val kotlinAttribute: Attribute<Boolean> by rootProject.extra
val kotlinPluginProjects = configurations
.getByName("compileClasspath")
.allDependencies
.filterIsInstance<ProjectDependency>()
.filter { it -> it.attributes.getAttribute(kotlinAttribute) != null }
.map { it.path }
kotlinPluginProjects.forEach { evaluationDependsOn(it) }
val ideLibs: Configuration by
configurations.creating {
isCanBeResolved = true
isCanBeConsumed = false
}
configure<EclipseModel> {
project {
name = "Template-OG-Plugin"
}
classpath {
plusConfigurations.add(ideLibs)
}
}
val ideLibDir = layout.buildDirectory.dir("ide-libs")
val hashRegex = Regex("-[0-9a-fA-F]{10}(?=\\.jar$)") // Black magic.
val copyTasks = mutableListOf<TaskProvider<Copy>>()
kotlinPluginProjects.forEach { path ->
val sub = project(path)
val shadowJarProv = sub.tasks.named("shadowJar")
val copyTask =
tasks.register<Copy>("ideCopy${sub.name.replaceFirstChar(Char::titlecase)}") {
dependsOn(shadowJarProv)
from(shadowJarProv)
into(ideLibDir)
rename { it.replace(hashRegex, "") } // Remove git commit hash from jarfile.
}
copyTasks += copyTask
}
/* Ensure the jars exist before .classpath is generated */
tasks.named("eclipseClasspath").configure { dependsOn(copyTasks) }
/* Supply those jars to the ideLibs configuration (after evaluation) */
afterEvaluate { dependencies { add("ideLibs", fileTree(ideLibDir) { include("*.jar") }) } }
val injectIdeLibs =
tasks.register("injectIdeLibs") {
dependsOn("eclipse") // run after all eclipse files are generated
doLast {
val cpFile = file(".classpath")
if (!cpFile.exists()) return@doLast
val jars = fileTree(ideLibDir.get()) { include("*.jar") }.files.sortedBy { it.name }
if (jars.isEmpty()) return@doLast
// Parse DOM
val dbf = DocumentBuilderFactory.newInstance()
val doc = dbf.newDocumentBuilder().parse(cpFile)
val root = doc.documentElement
// Helper to see if an entry already exists
fun exists(path: String): Boolean =
root.getElementsByTagName("classpathentry").let { list ->
(0 until list.length).any { i ->
val n = list.item(i)
val kind = n.attributes?.getNamedItem("kind")?.nodeValue
val p = n.attributes?.getNamedItem("path")?.nodeValue
kind == "lib" && p == path
}
}
// Remove any entry that points to ide-libs dir (folder or jars) to avoid dupes
val toRemove = mutableListOf<org.w3c.dom.Node>()
val list = root.getElementsByTagName("classpathentry")
val dirPath = ideLibDir.get().asFile.absolutePath
for (i in 0 until list.length) {
val n = list.item(i)
val kind = n.attributes?.getNamedItem("kind")?.nodeValue
val p = n.attributes?.getNamedItem("path")?.nodeValue ?: ""
if (kind == "lib" && (p == dirPath || p.startsWith("$dirPath/"))) {
toRemove += n
}
}
toRemove.forEach { root.removeChild(it) }
// Append our jar entries LAST
jars.forEach { f ->
val abs = f.absolutePath
if (!exists(abs)) {
val entry = doc.createElement("classpathentry")
entry.setAttribute("kind", "lib")
entry.setAttribute("path", abs)
root.appendChild(entry)
}
}
// Write back pretty
val tf =
TransformerFactory.newInstance().newTransformer().apply {
setOutputProperty(OutputKeys.INDENT, "yes")
setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "1")
setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")
setOutputProperty(OutputKeys.ENCODING, "UTF-8")
}
Files.newBufferedWriter(cpFile.toPath()).use { w -> tf.transform(DOMSource(doc), StreamResult(w)) }
}
}
tasks.named("eclipse").configure { finalizedBy(injectIdeLibs) }
val eclipseModel = extensions.getByType<EclipseModel>()
fun Project.addResolvableEclipseConfigs() {
val jarAttr =
objects.named(org.gradle.api.attributes.LibraryElements::class, org.gradle.api.attributes.LibraryElements.JAR)
val apiAttr = objects.named(org.gradle.api.attributes.Usage::class, org.gradle.api.attributes.Usage.JAVA_API)
val compileOnlyRes =
configurations.create("eclipseCompileOnly") {
extendsFrom(configurations.getByName("compileOnly"))
isCanBeResolved = true
isCanBeConsumed = false
attributes.attribute(org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE, apiAttr)
attributes.attribute(org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, jarAttr)
}
val compileOnlyApiRes =
configurations.create("eclipseCompileOnlyApi") {
extendsFrom(configurations.getByName("compileOnlyApi"))
isCanBeResolved = true
isCanBeConsumed = false
attributes.attribute(org.gradle.api.attributes.Usage.USAGE_ATTRIBUTE, apiAttr)
attributes.attribute(org.gradle.api.attributes.LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, jarAttr)
}
eclipseModel.classpath.plusConfigurations.addAll(listOf(compileOnlyRes, compileOnlyApiRes))
}
addResolvableEclipseConfigs()
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+251
View File
@@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
Vendored
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+339
View File
@@ -0,0 +1,339 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
Binary file not shown.
+1
Submodule libs/Utilities-OG added at 8b5c86cc9c.
-191
View File
@@ -1,191 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.josemmo.bukkit.plugin</groupId>
<artifactId>YamipaPlugin</artifactId>
<version>1.3.2-SNAPSHOT-b2</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>spigot-repo</id>
<url>https://hub.spigotmc.org/nexus/content/groups/public/</url>
</repository>
<repository>
<id>dmulloy2-repo</id>
<url>https://repo.dmulloy2.net/nexus/repository/public/</url>
</repository>
<repository>
<id>minecraft-libraries</id>
<url>https://libraries.minecraft.net</url>
</repository>
<repository>
<id>enginehub-repo</id>
<url>https://maven.enginehub.org/repo/</url>
</repository>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependencies>
<!-- https://hub.spigotmc.org/nexus/content/groups/public/org/spigotmc/spigot-api/ -->
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- https://repo.dmulloy2.net/service/rest/repository/browse/public/com/comphenix/protocol/ProtocolLib/ -->
<dependency>
<groupId>com.comphenix.protocol</groupId>
<artifactId>ProtocolLib</artifactId>
<version>5.2.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<!-- https://github.com/Mojang/brigadier/releases -->
<dependency>
<groupId>com.mojang</groupId>
<artifactId>brigadier</artifactId>
<version>1.0.18</version>
<scope>provided</scope>
</dependency>
<!-- https://central.sonatype.com/artifact/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>
<artifactId>worldguard-bukkit</artifactId>
<version>7.0.9</version>
<scope>provided</scope>
</dependency>
<!-- https://jitpack.io/com/github/TechFortress/GriefPrevention/ -->
<dependency>
<groupId>com.github.TechFortress</groupId>
<artifactId>GriefPrevention</artifactId>
<version>16.18.3</version>
<scope>provided</scope>
</dependency>
<!-- https://jitpack.io/com/github/TownyAdvanced/towny/ -->
<dependency>
<groupId>com.github.TownyAdvanced</groupId>
<artifactId>towny</artifactId>
<version>0.100.2.11</version>
<scope>provided</scope>
</dependency>
<!-- https://jitpack.io/com/github/Angeschossen/LandsAPI/ -->
<dependency>
<groupId>com.github.angeschossen</groupId>
<artifactId>LandsAPI</artifactId>
<version>7.1.12</version>
<scope>provided</scope>
</dependency>
<!-- https://central.sonatype.com/artifact/org.jetbrains/annotations -->
<dependency>
<groupId>org.jetbrains</groupId>
<artifactId>annotations</artifactId>
<version>24.1.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<finalName>${project.name}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>shade</id>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/**</exclude>
<exclude>LICENSE</exclude>
</excludes>
</filter>
</filters>
<relocations>
<relocation>
<pattern>org.bstats</pattern>
<shadedPattern>${project.groupId}.bstats</shadedPattern>
</relocation>
</relocations>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete file="${project.build.directory}/${project.build.finalName}-shaded.jar" />
<delete file="${project.build.directory}/original-${project.build.finalName}.jar" />
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
+13
View File
@@ -0,0 +1,13 @@
rootProject.name = "Yamipa-OG"
ProcessBuilder("sh", "bootstrap.sh").directory(rootDir).inheritIO().start().let {
if (it.waitFor() != 0) throw GradleException("bootstrap.sh failed")
}
file("libs")
.listFiles()
?.filter { it.isDirectory && !it.name.startsWith(".") }
?.forEach { dir ->
include(":libs:${dir.name}")
project(":libs:${dir.name}").projectDir = dir
}
@@ -1,118 +1,129 @@
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 java.awt.Color;
import java.nio.file.Path;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.event.HandlerList;
import org.bukkit.plugin.java.JavaPlugin;
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 io.josemmo.bukkit.plugin.commands.ImageCommandBridge;
import io.josemmo.bukkit.plugin.renderer.FakeEntity;
import io.josemmo.bukkit.plugin.renderer.FakeImage;
import io.josemmo.bukkit.plugin.renderer.FakeMap;
import io.josemmo.bukkit.plugin.renderer.ImageRenderer;
import io.josemmo.bukkit.plugin.renderer.ItemService;
import io.josemmo.bukkit.plugin.storage.ImageStorage;
import net.trueog.utilitiesog.UtilitiesOG;
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;
}
public static String getPrefix() {
return "&7[&eYamipa&f-&4OG&7] ";
}
/**
* Get image storage instance
*
* @return Image storage instance
*/
public @NotNull ImageStorage getStorage() {
Objects.requireNonNull(storage, "Cannot get storage instance if plugin is not running");
return storage;
}
/**
* Get image renderer instance
*
* @return Image renderer instance
*/
public @NotNull ImageRenderer getRenderer() {
Objects.requireNonNull(renderer, "Cannot get renderer instance if plugin is not running");
return renderer;
}
/**
* Get internal tasks scheduler
*
* @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;
return scheduler;
}
@Override
public void onLoad() {
INSTANCE = this;
instance = this;
}
@Override
public void onEnable() {
// Initialize logger
verbose = getConfig().getBoolean("verbose", false);
if (verbose) {
LOGGER.info("Running on VERBOSE mode");
info("Running on VERBOSE mode");
}
// Register plugin commands
ImageCommandBridge.register(this);
// Read plugin configuration paths
Path basePath = getDataFolder().toPath();
String imagesPath = getConfig().getString("images-path", "images");
String cachePath = getConfig().getString("cache-path", "cache");
String dataPath = getConfig().getString("data-path", "images.dat");
final Path basePath = getDataFolder().toPath();
final String imagesPath = getConfig().getString("images-path", "images");
final String cachePath = getConfig().getString("cache-path", "cache");
final 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
);
storage = new ImageStorage(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);
final boolean animateImages = getConfig().getBoolean("animate-images", true);
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,66 +134,119 @@ 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
Function<Integer, String> toStats = number -> {
if (number >= 1000) return "1000+";
if (number >= 500) return "500-999";
if (number >= 100) return "100-499";
if (number >= 50) return "50-99";
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.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) {
UtilitiesOG.logToConsole(getPrefix(), message);
} else {
UtilitiesOG.logToConsole(getPrefix(), message);
e.printStackTrace();
}
}
/**
* 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);
}
}
@@ -1,179 +1,244 @@
package io.josemmo.bukkit.plugin.commands;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.josemmo.bukkit.plugin.commands.arguments.Argument;
import io.josemmo.bukkit.plugin.utils.Internals;
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;
import java.util.function.Predicate;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import com.mojang.brigadier.builder.ArgumentBuilder;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import io.josemmo.bukkit.plugin.YamipaPlugin;
import io.josemmo.bukkit.plugin.commands.arguments.Argument;
import io.josemmo.bukkit.plugin.utils.Internals;
import net.trueog.utilitiesog.UtilitiesOG;
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<>();
/**
* Command constructor
*
* @param name Command literal name
*/
public Command(@NotNull String name) {
this.name = name;
}
/**
* Add argument to command
* @param argument Argument instance
* @return This instance
*
* @param argument Argument instance
* @return This instance
*/
public @NotNull Command withArgument(@NotNull Argument argument) {
arguments.add(argument);
return this;
}
/**
* Add requirement handler to this command
* @param handler Handler to determine command availability
* @return This instance
*
* @param handler Handler to determine command availability
* @return This instance
*/
public @NotNull Command withRequirement(@NotNull Predicate<CommandSender> handler) {
requirementHandler = handler;
return this;
}
/**
* Add permission requirement to this command
* @param permissions Permission names (match as least one)
* @return This instance
*
* @param permissions Permission names (match as least one)
* @return This instance
*/
public @NotNull Command withPermission(@NotNull String ...permissions) {
public @NotNull Command withPermission(@NotNull String... permissions) {
return withRequirement(sender -> {
for (String permission : permissions) {
if (sender.hasPermission(permission)) {
return true;
}
}
return false;
});
}
/**
* Add handler that will be executed when the command gets called
* @param handler Command handler
* @return This instance
*
* @param handler Command handler
* @return This instance
*/
public @NotNull Command executes(@NotNull BiConsumer<CommandSender, Object[]> handler) {
executesHandler = handler;
return this;
}
/**
* Add handler that will be executed when the command gets called by a player
* @param handler Command handler
* @return This instance
*
* @param handler Command handler
* @return This instance
*/
@SuppressWarnings("UnusedReturnValue")
public @NotNull Command executesPlayer(@NotNull BiConsumer<Player, Object[]> handler) {
executesPlayerHandler = handler;
return this;
}
/**
* Add subcommand to this command
* @param name Subcommand literal name
* @return The new subcommand
*
* @param name Subcommand literal name
* @return The new subcommand
*/
public @NotNull Command addSubcommand(@NotNull String name) {
Command subcommand = new Command(name);
final Command subcommand = new Command(name);
subcommands.add(subcommand);
return subcommand;
}
/**
* Build command and all of its children
*
* @return Literal argument builder instance
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "rawtypes", "unchecked" })
public @NotNull LiteralArgumentBuilder<?> build() {
LiteralArgumentBuilder<?> root = LiteralArgumentBuilder.literal(name);
final LiteralArgumentBuilder<?> root = LiteralArgumentBuilder.literal(name);
// Add arguments and execution handler
buildElement(root, 0);
// Add subcommands
for (Command subcommand : subcommands) {
root.then((ArgumentBuilder) subcommand.build());
}
subcommands.forEach(subcommand -> root.then((ArgumentBuilder) subcommand.build()));
return root;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "rawtypes", "unchecked" })
private @NotNull ArgumentBuilder buildElement(@NotNull ArgumentBuilder parent, int argIndex) {
// Attach requirement handler to each command element
parent.requires(source -> {
CommandSender sender = Internals.getBukkitSender(source);
final CommandSender sender = Internals.getBukkitSender(source);
return requirementHandler.test(sender);
});
// 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 -> {
CommandSender sender = Internals.getBukkitSender(ctx.getSource());
sender.sendMessage(ChatColor.RED + "Missing required arguments");
parent.then(buildElement(arguments.get(argIndex).build(), argIndex + 1)).executes(ctx -> {
final CommandSender sender = Internals.getBukkitSender(ctx.getSource());
final String argsErrorMessage = "&cERROR: Missing required arguments!";
if (sender instanceof Player) {
UtilitiesOG.trueogMessage((Player) sender, argsErrorMessage);
} else {
UtilitiesOG.logToConsole(YamipaPlugin.getPrefix(), argsErrorMessage);
}
return 1;
});
return parent;
}
// Attach execution handler to last command element
if (executesPlayerHandler != null) {
parent.executes(ctx -> {
CommandSender sender = Internals.getBukkitSender(ctx.getSource());
final CommandSender sender = Internals.getBukkitSender(ctx.getSource());
if (sender instanceof Player) {
executesPlayerHandler.accept((Player) sender, getArgumentValues(sender, ctx));
} else {
sender.sendMessage(ChatColor.RED + "Only in-game players can execute this command!");
UtilitiesOG.logToConsole(YamipaPlugin.getPrefix(),
"Only in-game players can execute this command!");
}
return 0;
});
} else if (executesHandler != null) {
parent.executes(ctx -> {
CommandSender sender = Internals.getBukkitSender(ctx.getSource());
final CommandSender sender = Internals.getBukkitSender(ctx.getSource());
executesHandler.accept(sender, getArgumentValues(sender, ctx));
return 0;
});
}
return parent;
}
private @NotNull Object[] getArgumentValues(
@NotNull CommandSender sender,
@NotNull CommandContext<?> ctx
) throws CommandSyntaxException {
List<Object> argValues = new ArrayList<>();
private @NotNull Object[] getArgumentValues(@NotNull CommandSender sender, @NotNull CommandContext<?> ctx)
throws CommandSyntaxException
{
final List<Object> argValues = new ArrayList<>();
argValues.add(ctx.getNodes().get(0).getNode().getName());
for (Argument argument : arguments) {
Object value = ctx.getArgument(argument.getName(), Object.class);
value = argument.parse(sender, value);
argValues.add(value);
}
return argValues.toArray(new Object[0]);
}
}
}
@@ -1,485 +1,604 @@
package io.josemmo.bukkit.plugin.commands;
import io.josemmo.bukkit.plugin.YamipaPlugin;
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;
import org.bukkit.*;
import org.bukkit.block.BlockFace;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.PluginDescriptionFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.imageio.ImageIO;
import java.awt.Dimension;
import java.io.IOException;
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.*;
import java.util.Date;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import javax.imageio.ImageIO;
import org.apache.commons.lang3.StringUtils;
import org.bukkit.Bukkit;
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.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import io.josemmo.bukkit.plugin.YamipaPlugin;
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.utils.ActionBar;
import io.josemmo.bukkit.plugin.utils.Permissions;
import io.josemmo.bukkit.plugin.utils.SelectBlockTask;
import net.trueog.utilitiesog.UtilitiesOG;
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;
private static void sendMessage(@NotNull CommandSender sender, @NotNull String message) {
if (sender instanceof Player) {
UtilitiesOG.trueogMessage((Player) sender, message);
} else {
UtilitiesOG.logToConsole(YamipaPlugin.getPrefix(), message);
}
}
public static void showHelp(@NotNull CommandSender s, @NotNull String commandName) {
String cmd = "/" + commandName;
s.sendMessage(ChatColor.BOLD + "=== Yamipa Plugin Help ===");
s.sendMessage(ChatColor.AQUA + cmd + ChatColor.RESET + " - Show this help");
final String cmd = "/" + commandName;
sendMessage(s, "&l=== Yamipa-OG Plugin Help ===");
sendMessage(s, "&b" + cmd + "&r - Show this help");
if (s.hasPermission("yamipa.command.clear") || s.hasPermission("yamipa.clear")) {
s.sendMessage(ChatColor.AQUA + cmd + " clear <x z w> <r> [<player>]" + ChatColor.RESET + " - Remove placed images");
sendMessage(s, "&b" + cmd + " clear <x z w> <r> [<player>]&r - Remove placed images");
}
if (s.hasPermission("yamipa.command.describe") || s.hasPermission("yamipa.describe")) {
s.sendMessage(ChatColor.AQUA + cmd + " describe" + ChatColor.RESET + " - Describe placed image");
sendMessage(s, "&b" + cmd + " describe&r - Describe placed image");
}
if (s.hasPermission("yamipa.command.download") || s.hasPermission("yamipa.download")) {
s.sendMessage(ChatColor.AQUA + cmd + " download <url> <filename>" + ChatColor.RESET + " - Download image");
sendMessage(s, "&b" + cmd + " download <url> <filename>&r - Download image");
}
if (s.hasPermission("yamipa.command.give") || s.hasPermission("yamipa.give")) {
s.sendMessage(ChatColor.AQUA + cmd + " give <p> <filename> <#> <w> [<h>] [<f>]" + ChatColor.RESET + " - Give items");
sendMessage(s, "&b" + cmd + " give <p> <filename> <#> <w> [<h>] [<f>]&r - Give items");
}
if (s.hasPermission("yamipa.command.list") || s.hasPermission("yamipa.list")) {
s.sendMessage(ChatColor.AQUA + cmd + " list [<page>]" + ChatColor.RESET + " - List all images");
sendMessage(s, "&b" + cmd + " list [<page>]&r - List all images");
}
if (s.hasPermission("yamipa.command.place") || s.hasPermission("yamipa.place")) {
s.sendMessage(ChatColor.AQUA + cmd + " place <filename> <w> [<h>] [<f>]" + ChatColor.RESET + " - Place image");
sendMessage(s, "&b" + cmd + " place <filename> <w> [<h>] [<f>]&r - Place image");
}
if (s.hasPermission("yamipa.command.remove.own") || s.hasPermission("yamipa.remove")) {
s.sendMessage(ChatColor.AQUA + cmd + " remove" + ChatColor.RESET + " - Remove a single placed image");
sendMessage(s, "&b" + cmd + " remove&r - Remove a single placed image");
}
if (s.hasPermission("yamipa.command.top") || s.hasPermission("yamipa.top")) {
s.sendMessage(ChatColor.AQUA + cmd + " top" + ChatColor.RESET + " - List players with the most images");
sendMessage(s, "&b" + cmd + " top&r - List players with the most images");
}
}
public static void listImages(@NotNull CommandSender sender, int page) {
ImageStorage storage = YamipaPlugin.getInstance().getStorage();
List<String> filenames = storage.getFilenames(sender);
int numOfImages = filenames.size();
final String[] filenames = YamipaPlugin.getInstance().getStorage().getAllFilenames();
final int numOfImages = filenames.length;
// Are there any images available?
if (numOfImages == 0) {
sender.sendMessage(ChatColor.RED + "No images found in the images directory");
sendMessage(sender, "&cNo images found in the images directory");
return;
}
// Is the page number valid?
int firstImageIndex = Math.max(page-1, 0) * ITEMS_PER_PAGE;
final int firstImageIndex = Math.max(page - 1, 0) * ITEMS_PER_PAGE;
if (firstImageIndex >= numOfImages) {
sender.sendMessage(ChatColor.RED + "Page " + page + " not found");
sendMessage(sender, "&cPage " + page + " not found");
return;
}
// Render list of images
int stopImageIndex = (page == 0) ? numOfImages : Math.min(numOfImages, firstImageIndex+ITEMS_PER_PAGE);
final int stopImageIndex = (page == 0) ? numOfImages : Math.min(numOfImages, firstImageIndex + ITEMS_PER_PAGE);
if (page > 0) {
int maxPage = (int) Math.ceil((float) numOfImages / ITEMS_PER_PAGE);
sender.sendMessage("=== Page " + page + " out of " + maxPage + " ===");
final int maxPage = (int) Math.ceil((float) numOfImages / ITEMS_PER_PAGE);
sendMessage(sender, "=== Page " + page + " out of " + maxPage + " ===");
}
for (int i=firstImageIndex; i<stopImageIndex; ++i) {
sender.sendMessage(ChatColor.GOLD + filenames.get(i));
for (int i = firstImageIndex; i < stopImageIndex; ++i) {
sendMessage(sender, "&6" + 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;
}
final YamipaPlugin plugin = YamipaPlugin.getInstance();
// Validate destination file
if (!destPath.startsWith(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");
final Path basePath = Paths.get(plugin.getStorage().getBasePath());
final Path destPath = basePath.resolve(filename);
if (!destPath.getParent().equals(basePath)) {
sendMessage(sender, "&cNot a valid destination filename");
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");
sendMessage(sender, "&cThere's already a file with that name");
return;
}
// Validate and fix remote URL
URL url;
String referrer = null;
try {
url = new URL(rawUrl);
url = new URL(rawUrl);
// Giphy.com
if (url.getHost().equals("giphy.com")) {
String path = url.getPath();
String id = path.substring(path.lastIndexOf('-')+1);
if ("giphy.com".equals(url.getHost())) {
final String path = url.getPath();
final String id = StringUtils.substring(path, path.lastIndexOf('-') + 1);
url = new URL("https://media.giphy.com/media/" + id + "/giphy.gif");
referrer = "https://giphy.com/";
}
// Imgur.com
if (url.getHost().equals("imgur.com")) {
String[] parts = url.getPath().replaceAll("^/|/$", "").split("/");
if (parts.length == 2 && (parts[0].equals("a") || parts[0].equals("gallery"))) {
if ("imgur.com".equals(url.getHost())) {
final String[] parts = url.getPath().replaceAll("^/|/$", "").split("/");
if (parts.length == 2 && ("a".equals(parts[0]) || "gallery".equals(parts[0]))) {
url = new URL("https://imgur.com/a/" + parts[1] + "/zip");
referrer = "https://imgur.com/a/" + parts[1];
} else {
url = new URL("https://imgur.com/download/" + parts[parts.length-1] + "/");
referrer = "https://imgur.com/" + parts[parts.length-1];
url = new URL("https://imgur.com/download/" + parts[parts.length - 1] + "/");
referrer = "https://imgur.com/" + parts[parts.length - 1];
}
}
} catch (MalformedURLException e) {
sender.sendMessage(ChatColor.RED + "The remote URL is not valid");
} catch (MalformedURLException malformedURLException) {
sendMessage(sender, "&cThe remote URL is not valid");
return;
}
// Download and validate remote file
final URL finalUrl = url;
final String finalReferrer = referrer;
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
try {
URLConnection conn = finalUrl.openConnection();
PluginDescriptionFile desc = plugin.getDescription();
final URLConnection conn = finalUrl.openConnection();
conn.setRequestProperty("Accept", "*/*");
conn.setRequestProperty("User-Agent", desc.getName() + "/" + desc.getVersion());
conn.setRequestProperty("User-Agent",
plugin.getPluginMeta().getName() + "/" + plugin.getPluginMeta().getVersion());
if (finalReferrer != null) {
conn.setRequestProperty("Referer", finalReferrer);
}
// Download file
sender.sendMessage("Downloading file...");
Files.createDirectories(destPath.getParent());
sendMessage(sender, "Downloading file...");
Files.copy(conn.getInputStream(), destPath);
// Validate downloaded file
if (ImageIO.read(destPath.toFile()) == null) {
throw new IllegalArgumentException("The downloaded file is not a valid image");
}
// Notify sender
sender.sendMessage(ChatColor.GREEN + "Done!");
sendMessage(sender, "&aDone!");
} 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());
sendMessage(sender, "&cAn error occurred trying to download the remote file");
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());
sendMessage(sender, "&c" + e.getMessage());
}
});
}
public static void placeImage(
@NotNull Player player,
@NotNull ImageFile image,
int width,
int height,
int flags
) {
public static void placeImage(@NotNull Player player, @NotNull ImageFile image, int width, int height, int flags) {
// Get image size in blocks
Dimension sizeInPixels = image.getSize();
final Dimension sizeInPixels = image.getSize();
if (sizeInPixels == null) {
player.sendMessage(ChatColor.RED + "The requested file is not a valid image");
UtilitiesOG.trueogMessage(player, "&cThe 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);
final SelectBlockTask task = new SelectBlockTask(player);
task.onSuccess((location, face) -> placeImage(player, image, width, finalHeight, flags, location, face));
task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image placing canceled"));
task.onFailure(() -> ActionBar.send(player, "&cImage placing canceled"));
task.run("Right click a block to continue");
}
public static boolean placeImage(
@NotNull Player player,
@NotNull ImageFile image,
int width,
int height,
int flags,
@NotNull Location location,
@NotNull BlockFace face
) {
ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
public static boolean placeImage(@NotNull Player player, @NotNull ImageFile image, int width, int height, int flags,
@NotNull Location location, @NotNull BlockFace face)
{
final ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
// Create new fake image instance
Rotation rotation = FakeImage.getRotationFromPlayerEyesight(face, player.getEyeLocation());
FakeImage fakeImage = new FakeImage(image.getFilename(), location, face, rotation,
width, height, new Date(), player, flags);
final Rotation rotation = FakeImage.getRotationFromPlayerEyesight(face, player.getEyeLocation());
final FakeImage fakeImage = new FakeImage(image.getName(), location, face, rotation, width, height, new Date(),
player, flags);
// Make sure image can be placed
for (Location loc : fakeImage.getAllLocations()) {
if (!Permissions.canBuild(player, loc)) {
ActionBar.send(player, ChatColor.RED + "You're not allowed to place an image here!");
ActionBar.send(player, "&cYou're not allowed to place an image here!");
return false;
}
if (renderer.getImage(loc, face) != null) {
ActionBar.send(player, ChatColor.RED + "There's already an image there!");
ActionBar.send(player, "&cThere's already an image there!");
return false;
}
}
// Show loading status to player
ActionBar loadingActionBar = ActionBar.repeat(player, ChatColor.AQUA + "Loading image...");
final ActionBar loadingActionBar = ActionBar.repeat(player, "&bLoading image...");
fakeImage.setOnLoadedListener(loadingActionBar::clear);
// Add fake image to renderer
renderer.addImage(fakeImage);
return true;
}
public static void removeImage(@NotNull Player player) {
SelectBlockTask task = new SelectBlockTask(player);
final SelectBlockTask task = new SelectBlockTask(player);
task.onSuccess((location, face) -> {
FakeImage image = YamipaPlugin.getInstance().getRenderer().getImage(location, face);
final FakeImage image = YamipaPlugin.getInstance().getRenderer().getImage(location, face);
if (image == null) {
ActionBar.send(player, ChatColor.RED + "That is not a valid image!");
ActionBar.send(player, "&cThat is not a valid image!");
return;
}
// Check player's command permissions
if (
!player.getUniqueId().equals(image.getPlacedBy().getUniqueId()) &&
!player.hasPermission("yamipa.command.remove") &&
!player.hasPermission("yamipa.remove")
) {
ActionBar.send(player, ChatColor.RED + "You cannot remove images from other players!");
if (!player.getUniqueId().equals(image.getPlacedBy().getUniqueId())
&& !player.hasPermission("yamipa.command.remove") && !player.hasPermission("yamipa.remove"))
{
ActionBar.send(player, "&cYou cannot remove images from other players!");
return;
}
// Attempt to remove image
removeImage(player, image);
});
task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image removing canceled"));
task.onFailure(() -> ActionBar.send(player, "&cImage removing canceled"));
task.run("Right click an image to continue");
}
public static boolean removeImage(@NotNull Player player, @NotNull FakeImage image) {
// Check block permissions
for (Location loc : image.getAllLocations()) {
if (!Permissions.canDestroy(player, loc)) {
ActionBar.send(player, ChatColor.RED + "You're not allowed to remove this image!");
ActionBar.send(player, "&cYou're not allowed to remove this image!");
return false;
}
}
// Trigger image removal
YamipaPlugin.getInstance().getRenderer().removeImage(image);
return true;
}
public static void clearImages(
@NotNull CommandSender sender,
@NotNull Location origin,
int radius,
@Nullable OfflinePlayer placedBy
) {
ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
public static void clearImages(@NotNull CommandSender sender, @NotNull Location origin, int radius,
@Nullable OfflinePlayer placedBy)
{
final ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
// Get images in area
Set<FakeImage> images = renderer.getImages(
Objects.requireNonNull(origin.getWorld()),
origin.getBlockX()-radius+1,
origin.getBlockX()+radius-1,
origin.getBlockZ()-radius+1,
origin.getBlockZ()+radius-1
);
final Set<FakeImage> images = renderer.getImages(origin.getWorld(), origin.getBlockX() - radius + 1,
origin.getBlockX() + radius - 1, origin.getBlockZ() - radius + 1, origin.getBlockZ() + radius - 1);
// Filter out images not placed by targeted player
if (placedBy != null) {
UUID target = placedBy.getUniqueId();
final UUID target = placedBy.getUniqueId();
images.removeIf(image -> !target.equals(image.getPlacedBy().getUniqueId()));
}
// Filter out images outside the permission scope of the sender
if (sender instanceof Player) {
Player senderAsPlayer = (Player) sender;
if (sender instanceof Player senderAsPlayer) {
images.removeIf(image -> {
for (Location loc : image.getAllLocations()) {
if (!Permissions.canDestroy(senderAsPlayer, loc)) {
return true;
}
}
return false;
});
}
// Remove found images
for (FakeImage image : images) {
renderer.removeImage(image);
}
sender.sendMessage("Removed " + images.size() + " placed image(s)");
images.forEach(renderer::removeImage);
sendMessage(sender, "Removed " + images.size() + " placed image(s)");
}
public static void describeImage(@NotNull Player player) {
ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
final ImageRenderer renderer = YamipaPlugin.getInstance().getRenderer();
// Ask user to select fake image
SelectBlockTask task = new SelectBlockTask(player);
final SelectBlockTask task = new SelectBlockTask(player);
task.onSuccess((location, face) -> {
FakeImage image = renderer.getImage(location, face);
final FakeImage image = renderer.getImage(location, face);
if (image == null) {
ActionBar.send(player, ChatColor.RED + "That is not a valid image!");
ActionBar.send(player, "&cThat is not a valid image!");
return;
}
// Separate previous messages
player.sendMessage("");
UtilitiesOG.trueogMessage(player, "");
// Basic information
player.sendMessage(ChatColor.GOLD + "Filename: " + ChatColor.RESET + image.getFilename());
player.sendMessage(ChatColor.GOLD + "World: " + ChatColor.RESET +
image.getLocation().getChunk().getWorld().getName());
player.sendMessage(ChatColor.GOLD + "Coordinates: " + ChatColor.RESET +
image.getLocation().getBlockX() + ", " +
image.getLocation().getBlockY() + ", " +
image.getLocation().getBlockZ());
player.sendMessage(ChatColor.GOLD + "Block Face: " + ChatColor.RESET + image.getBlockFace());
player.sendMessage(ChatColor.GOLD + "Rotation: " + ChatColor.RESET + image.getRotation());
player.sendMessage(ChatColor.GOLD + "Dimensions: " + ChatColor.RESET +
image.getWidth() + "x" + image.getHeight() + " blocks");
UtilitiesOG.trueogMessage(player, "&6Filename: &r" + image.getFilename());
UtilitiesOG.trueogMessage(player, "&6World: &r" + image.getLocation().getChunk().getWorld().getName());
UtilitiesOG.trueogMessage(player, "&6Coordinates: &r" + image.getLocation().getBlockX() + ", "
+ image.getLocation().getBlockY() + ", " + image.getLocation().getBlockZ());
UtilitiesOG.trueogMessage(player, "&6Block Face: &r" + image.getBlockFace());
UtilitiesOG.trueogMessage(player, "&6Rotation: &r" + image.getRotation());
UtilitiesOG.trueogMessage(player,
"&6Dimensions: &r" + image.getWidth() + "x" + image.getHeight() + " blocks");
// Speed
int delay = image.getDelay() * 50;
String delayStr = (delay > 0) ? delay + " ms per step" : ChatColor.GRAY + "N/A";
player.sendMessage(ChatColor.GOLD + "Speed: " + ChatColor.RESET + delayStr);
final int delay = image.getDelay() * 50;
final String delayStr = (delay > 0) ? delay + " ms per step" : "&7N/A";
UtilitiesOG.trueogMessage(player, "&6Speed: &r" + delayStr);
// Placed At
String dateStr = (image.getPlacedAt() == null) ?
ChatColor.GRAY + "Some point in time" :
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(image.getPlacedAt());
player.sendMessage(ChatColor.GOLD + "Placed At: " + ChatColor.RESET + dateStr);
final String dateStr = (image.getPlacedAt() == null) ? "&7Some point in time"
: new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z").format(image.getPlacedAt());
UtilitiesOG.trueogMessage(player, "&6Placed At: &r" + dateStr);
// Placed By
String playerStr;
final String playerStr;
if (image.getPlacedBy().getUniqueId().equals(FakeImage.UNKNOWN_PLAYER_ID)) {
playerStr = ChatColor.GRAY + "Someone";
playerStr = "&7Someone";
} else if (image.getPlacedBy().getName() == null) {
playerStr = ChatColor.DARK_AQUA + image.getPlacedBy().getUniqueId().toString();
playerStr = "&3" + image.getPlacedBy().getUniqueId();
} else {
playerStr = image.getPlacedBy().getName();
}
player.sendMessage(ChatColor.GOLD + "Placed By: " + ChatColor.RESET + playerStr);
UtilitiesOG.trueogMessage(player, "&6Placed By: &r" + playerStr);
// Flags
String flagsStr = "";
if (image.hasFlag(FakeImage.FLAG_ANIMATABLE)) {
flagsStr += ChatColor.AQUA + "ANIM ";
flagsStr += "&bANIM ";
}
if (image.hasFlag(FakeImage.FLAG_REMOVABLE)) {
flagsStr += ChatColor.RED + "REMO ";
flagsStr += "&cREMO ";
}
if (image.hasFlag(FakeImage.FLAG_DROPPABLE)) {
flagsStr += ChatColor.LIGHT_PURPLE + "DROP ";
flagsStr += "&dDROP ";
}
if (image.hasFlag(FakeImage.FLAG_GLOWING)) {
flagsStr += ChatColor.GREEN + "GLOW ";
flagsStr += "&aGLOW ";
}
if (flagsStr.isEmpty()) {
flagsStr = ChatColor.GRAY + "N/A";
if (StringUtils.isEmpty(flagsStr)) {
flagsStr = "&7N/A";
}
player.sendMessage(ChatColor.GOLD + "Flags: " + ChatColor.RESET + flagsStr);
UtilitiesOG.trueogMessage(player, "&6Flags: &r" + flagsStr);
});
task.onFailure(() -> ActionBar.send(player, ChatColor.RED + "Image describing canceled"));
task.onFailure(() -> ActionBar.send(player, "&cImage describing canceled"));
task.run("Right click the image to describe");
}
public static void showTopPlayers(@NotNull CommandSender sender) {
UUID senderId = (sender instanceof Player) ? ((Player) sender).getUniqueId() : null;
Map<OfflinePlayer, Integer> stats = YamipaPlugin.getInstance().getRenderer().getImagesCountByPlayer();
final UUID senderId = (sender instanceof Player) ? ((Player) sender).getUniqueId() : null;
final Map<OfflinePlayer, Integer> stats = YamipaPlugin.getInstance().getRenderer().getImagesCountByPlayer();
// Render header
sender.sendMessage("=== Top players with the most placed images ===");
sendMessage(sender, "=== Top players with the most placed images ===");
if (stats.isEmpty()) {
sender.sendMessage(ChatColor.RED + "No one on this server has placed a single image!");
sendMessage(sender, "&cNo one on this server has placed a single image!");
return;
}
int rank = 0;
int printedLines = 0;
boolean hasShownSender = (senderId == null); // Assume sender has already been shown if it's not a player
for (Map.Entry<OfflinePlayer, Integer> item : stats.entrySet()) {
OfflinePlayer player = item.getKey();
int value = item.getValue();
final OfflinePlayer player = item.getKey();
final int value = item.getValue();
++rank;
// Skip line if irrelevant
if (player.getUniqueId().equals(senderId)) {
hasShownSender = true;
} else if (!hasShownSender && printedLines == ITEMS_PER_PAGE-1) {
} else if (!hasShownSender && printedLines == ITEMS_PER_PAGE - 1) {
continue; // Leave last line empty for sender rank
} else if (printedLines >= ITEMS_PER_PAGE) {
break; // Stop printing players when chat is filled
}
// Prepare player name or UUID
String playerName = (player.getName() == null) ?
ChatColor.GOLD + player.getUniqueId().toString() :
ChatColor.GREEN + player.getName();
final String playerName = (player.getName() == null) ? "&6" + player.getUniqueId()
: "&a" + player.getName();
// Render player line
sender.sendMessage(
"" + ChatColor.BOLD + (rank > 1000 ? "1000+" : rank) + ChatColor.RESET + ". " +
playerName + ChatColor.RESET +
ChatColor.GRAY + " - " + value + " " + (value == 1 ? "image" : "images")
);
sendMessage(sender, "&l" + (rank > 1000 ? "1000+" : rank) + "&r. " + playerName + "&r&7 - " + value + " "
+ (value == 1 ? "image" : "images"));
++printedLines;
}
}
public static void giveImageItems(
@NotNull CommandSender sender,
@NotNull Player player,
@NotNull ImageFile image,
int amount,
int width,
int height,
int flags
) {
public static void giveImageItems(@NotNull CommandSender sender, @NotNull Player player, @NotNull ImageFile image,
int amount, int width, int height, int flags)
{
// Get image size in blocks
Dimension sizeInPixels = image.getSize();
final Dimension sizeInPixels = image.getSize();
if (sizeInPixels == null) {
sender.sendMessage(ChatColor.RED + "The requested file is not a valid image");
sendMessage(sender, "&cThe requested file is not a valid image");
return;
}
if (height == 0) {
height = FakeImage.getProportionalHeight(sizeInPixels, sender, width);
height = FakeImage.getProportionalHeight(sizeInPixels, width);
}
// Create item stack
ItemStack itemStack = ItemService.getImageItem(image, amount, width, height, flags);
final ItemStack itemStack = ItemService.getImageItem(image, amount, width, height, flags);
// Add item stack to player's inventory
player.getInventory().addItem(itemStack);
sender.sendMessage(
ChatColor.ITALIC + "Added " + amount + " " +
(amount == 1 ? "image item" : "image items") +
" to " + player.getName() + "'s inventory"
);
sendMessage(sender, "&oAdded " + amount + " " + (amount == 1 ? "image item" : "image items") + " to "
+ player.getName() + "'s inventory");
}
}
}
@@ -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,16 +15,18 @@ 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" };
/**
* Register command
*
* @param plugin Plugin instance
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void register(@NotNull YamipaPlugin plugin) {
CommandDispatcher dispatcher = Internals.getDispatcher();
// Register command
@@ -34,184 +35,210 @@ public class ImageCommandBridge {
// Register aliases
for (String alias : COMMAND_ALIASES) {
LiteralCommandNode<?> aliasNode = new LiteralCommandNode(
alias,
commandNode.getCommand(),
commandNode.getRequirement(),
commandNode,
commandNode.getRedirectModifier(),
commandNode.isFork()
);
LiteralCommandNode<?> aliasNode = new LiteralCommandNode(alias, commandNode.getCommand(),
commandNode.getRequirement(), commandNode, commandNode.getRedirectModifier(), commandNode.isFork());
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, () -> {
fixPermissions(COMMAND_NAME);
for (String alias : COMMAND_ALIASES) {
fixPermissions(alias);
}
LOGGER.fine("Fixed command permissions");
try {
Internals.syncCommands();
} catch (RuntimeException e) {
plugin.warning("Failed to synchronize Bukkit commands with Brigadier");
plugin.log(java.util.logging.Level.FINE, e.getMessage(), e);
}
plugin.fine("Fixed command permissions");
});
}
@SuppressWarnings("CodeBlock2Expr")
private static @NotNull Command getRootCommand() {
Command root = new Command(COMMAND_NAME);
// Help command
root.withPermission(
"yamipa.command.clear", "yamipa.clear",
"yamipa.command.describe", "yamipa.describe",
"yamipa.command.download", "yamipa.download",
"yamipa.command.give", "yamipa.give",
"yamipa.command.list", "yamipa.list",
"yamipa.command.place", "yamipa.place",
"yamipa.command.remove.own", "yamipa.remove",
"yamipa.command.top", "yamipa.top"
)
.executes((sender, args) -> {
ImageCommand.showHelp(sender, (String) args[0]);
});
root.withPermission("yamipa.command.clear", "yamipa.clear", "yamipa.command.describe", "yamipa.describe",
"yamipa.command.download", "yamipa.download", "yamipa.command.give", "yamipa.give",
"yamipa.command.list", "yamipa.list", "yamipa.command.place", "yamipa.place",
"yamipa.command.remove.own", "yamipa.remove", "yamipa.command.top", "yamipa.top")
.executes((sender, args) ->
{
ImageCommand.showHelp(sender, (String) args[0]);
});
// Clear command
root.addSubcommand("clear")
.withPermission("yamipa.command.clear", "yamipa.clear")
.withArgument(new IntegerArgument("x"))
.withArgument(new IntegerArgument("z"))
.withArgument(new WorldArgument("world"))
.withArgument(new IntegerArgument("radius", 1))
.withArgument(new PlacedByArgument("placedBy"))
.executes((sender, args) -> {
Location origin = new Location((World) args[3], (int) args[1], 0, (int) args[2]);
ImageCommand.clearImages(sender, origin, (int) args[4], (OfflinePlayer) args[5]);
});
root.addSubcommand("clear")
.withPermission("yamipa.command.clear", "yamipa.clear")
.withArgument(new IntegerArgument("x"))
.withArgument(new IntegerArgument("z"))
.withArgument(new WorldArgument("world"))
.withArgument(new IntegerArgument("radius", 1))
.executes((sender, args) -> {
Location origin = new Location((World) args[3], (int) args[1], 0, (int) args[2]);
ImageCommand.clearImages(sender, origin, (int) args[4], null);
});
root.addSubcommand("clear").withPermission("yamipa.command.clear", "yamipa.clear")
.withArgument(new IntegerArgument("x")).withArgument(new IntegerArgument("z"))
.withArgument(new WorldArgument("world")).withArgument(new IntegerArgument("radius", 1))
.withArgument(new PlacedByArgument("placedBy")).executes((sender, args) ->
{
Location origin = new Location((World) args[3], (int) args[1], 0, (int) args[2]);
ImageCommand.clearImages(sender, origin, (int) args[4], (OfflinePlayer) args[5]);
});
root.addSubcommand("clear").withPermission("yamipa.command.clear", "yamipa.clear")
.withArgument(new IntegerArgument("x")).withArgument(new IntegerArgument("z"))
.withArgument(new WorldArgument("world")).withArgument(new IntegerArgument("radius", 1))
.executes((sender, args) ->
{
Location origin = new Location((World) args[3], (int) args[1], 0, (int) args[2]);
ImageCommand.clearImages(sender, origin, (int) args[4], null);
});
// Describe command
root.addSubcommand("describe")
.withPermission("yamipa.command.describe", "yamipa.describe")
.executesPlayer((player, __) -> {
ImageCommand.describeImage(player);
});
root.addSubcommand("describe").withPermission("yamipa.command.describe", "yamipa.describe")
.executesPlayer((player, __) ->
{
ImageCommand.describeImage(player);
});
// Download command
root.addSubcommand("download")
.withPermission("yamipa.command.download", "yamipa.download")
.withArgument(new StringArgument("url"))
.withArgument(new StringArgument("filename"))
.executes((sender, args) -> {
ImageCommand.downloadImage(sender, (String) args[1], (String) args[2]);
});
root.addSubcommand("download").withPermission("yamipa.command.download", "yamipa.download")
.withArgument(new StringArgument("url")).withArgument(new StringArgument("filename"))
.executes((sender, args) ->
{
ImageCommand.downloadImage(sender, (String) args[1], (String) args[2]);
});
// Give subcommand
root.addSubcommand("give")
.withPermission("yamipa.command.give", "yamipa.give")
.withArgument(new OnlinePlayerArgument("player"))
.withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("amount", 1, 64))
.withArgument(new ImageDimensionArgument("width"))
.withArgument(new ImageDimensionArgument("height"))
.withArgument(new ImageFlagsArgument("flags", FakeImage.DEFAULT_GIVE_FLAGS))
.executes((sender, args) -> {
ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
(int) args[4], (int) args[5], (int) args[6]);
});
root.addSubcommand("give")
.withPermission("yamipa.command.give", "yamipa.give")
.withArgument(new OnlinePlayerArgument("player"))
.withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("amount", 1, 64))
.withArgument(new ImageDimensionArgument("width"))
.withArgument(new ImageDimensionArgument("height"))
.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);
});
root.addSubcommand("give")
.withPermission("yamipa.command.give", "yamipa.give")
.withArgument(new OnlinePlayerArgument("player"))
.withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("amount", 1, 64))
.withArgument(new ImageDimensionArgument("width"))
.executes((sender, args) -> {
ImageCommand.giveImageItems(sender, (Player) args[1], (ImageFile) args[2], (int) args[3],
(int) args[4], 0, FakeImage.DEFAULT_GIVE_FLAGS);
});
root.addSubcommand("give").withPermission("yamipa.command.give", "yamipa.give")
.withArgument(new OnlinePlayerArgument("player")).withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("amount", 1, 64))
.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],
(int) args[4], (int) args[5], (int) args[6]);
});
root.addSubcommand("give").withPermission("yamipa.command.give", "yamipa.give")
.withArgument(new OnlinePlayerArgument("player")).withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("amount", 1, 64))
.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);
});
root.addSubcommand("give").withPermission("yamipa.command.give", "yamipa.give")
.withArgument(new OnlinePlayerArgument("player")).withArgument(new ImageFileArgument("filename"))
.withArgument(new IntegerArgument("amount", 1, 64))
.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);
});
// List subcommand
root.addSubcommand("list")
.withPermission("yamipa.command.list", "yamipa.list")
.withArgument(new IntegerArgument("page", 1))
.executes((sender, args) -> {
ImageCommand.listImages(sender, (int) args[1]);
});
root.addSubcommand("list")
.withPermission("yamipa.command.list", "yamipa.list")
.executes((sender, __) -> {
boolean isPlayer = (sender instanceof Player);
ImageCommand.listImages(sender, isPlayer ? 1 : 0);
});
root.addSubcommand("list").withPermission("yamipa.command.list", "yamipa.list")
.withArgument(new IntegerArgument("page", 1)).executes((sender, args) ->
{
ImageCommand.listImages(sender, (int) args[1]);
});
root.addSubcommand("list").withPermission("yamipa.command.list", "yamipa.list").executes((sender, __) -> {
boolean isPlayer = (sender instanceof Player);
ImageCommand.listImages(sender, isPlayer ? 1 : 0);
});
// Place subcommand
root.addSubcommand("place")
.withPermission("yamipa.command.place", "yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.withArgument(new ImageDimensionArgument("width"))
.withArgument(new ImageDimensionArgument("height"))
.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]);
});
root.addSubcommand("place")
.withPermission("yamipa.command.place", "yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.withArgument(new ImageDimensionArgument("width"))
.withArgument(new ImageDimensionArgument("height"))
.executesPlayer((player, args) -> {
ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], (int) args[3],
FakeImage.DEFAULT_PLACE_FLAGS);
});
root.addSubcommand("place")
.withPermission("yamipa.command.place", "yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.withArgument(new ImageDimensionArgument("width"))
.executesPlayer((player, args) -> {
ImageCommand.placeImage(player, (ImageFile) args[1], (int) args[2], 0,
FakeImage.DEFAULT_PLACE_FLAGS);
});
root.addSubcommand("place").withPermission("yamipa.command.place", "yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.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]);
});
root.addSubcommand("place").withPermission("yamipa.command.place", "yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.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);
});
root.addSubcommand("place").withPermission("yamipa.command.place", "yamipa.place")
.withArgument(new ImageFileArgument("filename"))
.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);
});
// Remove subcommand
root.addSubcommand("remove")
.withPermission("yamipa.command.remove.own", "yamipa.remove")
.executesPlayer((player, __) -> {
ImageCommand.removeImage(player);
});
root.addSubcommand("remove").withPermission("yamipa.command.remove.own", "yamipa.remove")
.executesPlayer((player, __) ->
{
ImageCommand.removeImage(player);
});
// Top subcommand
root.addSubcommand("top")
.withPermission("yamipa.command.top", "yamipa.top")
.executes((sender, __) -> {
ImageCommand.showTopPlayers(sender);
});
root.addSubcommand("top").withPermission("yamipa.command.top", "yamipa.top").executes((sender, __) -> {
ImageCommand.showTopPlayers(sender);
});
return root;
}
private static void fixPermissions(@NotNull String commandName) {
org.bukkit.command.Command command = Internals.getCommandMap().getCommand(commandName);
if (command != null) { // Command may have been aliased to null in "commands.yml"
command.setPermission(null);
}
}
}
@@ -4,63 +4,61 @@ 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;
/**
* Argument constructor
*
* @param name Argument name
*/
public Argument(@NotNull String name) {
this.name = name;
}
/**
* Get argument name
*
* @return Argument name
*/
public @NotNull String getName() {
return name;
}
/**
* 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
* @param rawValue Argument value provided by Brigadier
* @return Parsed argument value
*
* @param sender Command sender
* @param rawValue Argument value provided by Brigadier
* @return Parsed argument value
*/
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue)
throws CommandSyntaxException
{
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,41 +1,64 @@
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;
public class ImageFileArgument extends StringArgument {
/**
* Image File Argument constructor
*
* @param name Argument name
*/
public ImageFileArgument(@NotNull String name) {
super(name);
}
@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)) {
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue)
throws CommandSyntaxException
{
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;
@@ -13,58 +14,75 @@ import java.util.Collections;
import java.util.concurrent.CompletableFuture;
public class ImageFlagsArgument extends Argument {
private final int defaultFlags;
/**
* Image File Argument constructor
*
* @param name Argument name
* @param defaultFlags Default flags
*/
public ImageFlagsArgument(@NotNull String name, int defaultFlags) {
super(name);
this.defaultFlags = defaultFlags;
}
@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("-"))
);
input = input.substring(0, lastIndex+1);
int lastIndex = Collections
.max(Arrays.asList(input.lastIndexOf(","), input.lastIndexOf("+"), input.lastIndexOf("-")));
input = input.substring(0, lastIndex + 1);
// Add suggestions
String[] values = new String[] {"ANIM", "REMO", "DROP", "GLOW"};
String[] values = new String[] { "ANIM", "REMO", "DROP", "GLOW" };
for (String value : values) {
builder.suggest(input + value);
}
return builder.buildFuture();
}
@Override
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue)
throws CommandSyntaxException
{
String value = (String) rawValue;
int parsedValue = defaultFlags;
// Load flags from parts
String[] parts = value.split(",");
for (String part : parts) {
// Parse modifier
String modifier = "";
if (part.startsWith("+") || part.startsWith("-")) {
modifier = part.substring(0, 1);
part = part.substring(1);
}
// Parse flag name
int flag;
switch (part) {
case "ANIM":
flag = FakeImage.FLAG_ANIMATABLE;
break;
@@ -79,18 +97,28 @@ public class ImageFlagsArgument extends Argument {
break;
default:
throw newException("Unrecognized flag \"" + part + "\"");
}
// Apply flag to value
if (modifier.equals("+")) {
parsedValue |= flag;
} else if (modifier.equals("-")) {
parsedValue &= ~flag;
} else {
parsedValue = flag;
}
}
return parsedValue;
}
}
@@ -5,40 +5,53 @@ import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import org.jetbrains.annotations.NotNull;
public class IntegerArgument extends Argument {
private final int min;
private final int max;
/**
* Integer Argument constructor
*
* @param name Argument name
* @param min Minimum allowed value
* @param max Maximum allowed value
*/
public IntegerArgument(@NotNull String name, int min, int max) {
super(name);
this.min = min;
this.max = max;
}
/**
* Integer Argument constructor
*
* @param name Argument name
* @param min Minimum allowed value
*/
public IntegerArgument(@NotNull String name, int min) {
this(name, min, Integer.MAX_VALUE);
}
/**
* Integer Argument constructor
*
* @param name Argument name
*/
public IntegerArgument(@NotNull String name) {
this(name, Integer.MIN_VALUE, Integer.MAX_VALUE);
}
@Override
public @NotNull RequiredArgumentBuilder<?, ?> build() {
return RequiredArgumentBuilder.argument(name, IntegerArgumentType.integer(min, max));
}
}
@@ -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;
@@ -12,35 +14,62 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class OnlinePlayerArgument extends StringArgument {
/**
* Online player argument constructor
*
* @param name Argument name
*/
public OnlinePlayerArgument(@NotNull String name) {
super(name);
}
@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
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue)
throws CommandSyntaxException
{
Player player = getAllowedValues().get((String) rawValue);
if (player == null) {
throw newException("Expected online player (name or UUID)");
}
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()) {
values.put(player.getUniqueId().toString(), player);
values.put(player.getName(), player);
}
return values;
}
}
@@ -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;
@@ -13,38 +15,67 @@ import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class PlacedByArgument extends StringArgument {
/**
* Placed by Argument constructor
*
* @param name Argument name
*/
public PlacedByArgument(@NotNull String name) {
super(name);
}
@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
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue)
throws CommandSyntaxException
{
OfflinePlayer player = getAllowedValues().get((String) rawValue);
if (player == null) {
throw newException("Expected player with placed images (name or UUID)");
}
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();
for (OfflinePlayer player : renderer.getPlayersWithPlacedImages()) {
values.put(player.getUniqueId().toString(), player);
if (player.getName() != null) {
values.put(player.getName(), player);
}
}
return values;
}
}
@@ -5,16 +5,23 @@ import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import org.jetbrains.annotations.NotNull;
public class StringArgument extends Argument {
/**
* String Argument constructor
*
* @param name Argument name
*/
public StringArgument(@NotNull String name) {
super(name);
}
@Override
public @NotNull RequiredArgumentBuilder<?, ?> build() {
return RequiredArgumentBuilder.argument(name, StringArgumentType.string());
}
}
@@ -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;
@@ -10,28 +12,53 @@ import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
public class WorldArgument extends StringArgument {
/**
* World Argument constructor
*
* @param name Argument name
*/
public WorldArgument(@NotNull String name) {
super(name);
}
@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
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue) throws CommandSyntaxException {
public @NotNull Object parse(@NotNull CommandSender sender, @NotNull Object rawValue)
throws CommandSyntaxException
{
World world = Bukkit.getWorld((String) rawValue);
if (world == null) {
throw newException("Expected world name");
}
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();
}
}
@@ -1,25 +1,39 @@
package io.josemmo.bukkit.plugin.packets;
import org.jetbrains.annotations.NotNull;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.EnumWrappers;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import io.josemmo.bukkit.plugin.utils.Internals;
import org.jetbrains.annotations.NotNull;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.trueog.utilitiesog.UtilitiesOG;
public class ActionBarPacket extends PacketContainer {
private static final long serialVersionUID = 1L;
private static final boolean USE_TITLE = (Internals.MINECRAFT_VERSION < 17);
@SuppressWarnings("deprecation")
public ActionBarPacket() {
super(USE_TITLE ? PacketType.Play.Server.TITLE : PacketType.Play.Server.SET_ACTION_BAR_TEXT);
if (USE_TITLE) {
getTitleActions().write(0, EnumWrappers.TitleAction.ACTIONBAR);
}
}
public @NotNull ActionBarPacket setText(@NotNull String text) {
getChatComponents().write(0, WrappedChatComponent.fromText(text));
final String serialized = GsonComponentSerializer.gson().serialize(UtilitiesOG.trueogColorize(text));
getChatComponents().write(0, WrappedChatComponent.fromJson(serialized));
return this;
}
}
}
@@ -1,24 +1,42 @@
package io.josemmo.bukkit.plugin.packets;
import java.util.Collections;
import org.jetbrains.annotations.NotNull;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import io.josemmo.bukkit.plugin.utils.Internals;
import org.jetbrains.annotations.NotNull;
import java.util.Collections;
public class DestroyEntityPacket extends PacketContainer {
private static final long serialVersionUID = 1L;
public DestroyEntityPacket() {
super(PacketType.Play.Server.ENTITY_DESTROY);
}
public @NotNull DestroyEntityPacket setId(int id) {
if (Internals.MINECRAFT_VERSION < 17) { // Minecraft 1.16.x
getIntegerArrays().write(0, new int[]{id});
getIntegerArrays().write(0, new int[] { id });
} else if (Internals.MINECRAFT_VERSION < 17.1) { // Minecraft 1.17
getIntegers().write(0, id);
} else { // Minecraft 1.17.x
getIntLists().write(0, Collections.singletonList(id));
}
return this;
}
}
}
@@ -1,18 +1,23 @@
package io.josemmo.bukkit.plugin.packets;
import java.util.ArrayList;
import java.util.List;
import org.bukkit.Rotation;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.WrappedDataValue;
import com.comphenix.protocol.wrappers.WrappedDataWatcher;
import io.josemmo.bukkit.plugin.utils.Internals;
import org.bukkit.Rotation;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
public class EntityMetadataPacket extends PacketContainer {
private static final long serialVersionUID = 1L;
private static final boolean USE_DATA_WATCHER;
private static final int ITEM_INDEX;
private static final int ROTATION_INDEX;
@@ -20,68 +25,100 @@ public class EntityMetadataPacket extends PacketContainer {
private final List<WrappedDataValue> values = new ArrayList<>(); // For >= 1.19.3
static {
USE_DATA_WATCHER = (Internals.MINECRAFT_VERSION < 19.3f);
ITEM_INDEX = (Internals.MINECRAFT_VERSION < 17) ? 7 : 8;
ROTATION_INDEX = ITEM_INDEX + 1;
}
public EntityMetadataPacket() {
super(PacketType.Play.Server.ENTITY_METADATA);
}
public @NotNull EntityMetadataPacket setId(int id) {
getIntegers().write(0, id);
return this;
}
public @NotNull EntityMetadataPacket setFlags(byte flags) {
WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.get(Byte.class);
final WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.get(Byte.class);
if (USE_DATA_WATCHER) {
dataWatcher.setObject(new WrappedDataWatcher.WrappedDataWatcherObject(0, serializer), flags);
} else {
values.add(new WrappedDataValue(0, serializer, flags));
}
return this;
}
public @NotNull EntityMetadataPacket setInvisible(boolean invisible) {
int flags = invisible ? 0x20 : 0x00;
final int flags = invisible ? 0x20 : 0x00;
return setFlags((byte) flags);
}
public @NotNull EntityMetadataPacket setItem(@NotNull ItemStack item) {
WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.getItemStackSerializer(false);
final WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.getItemStackSerializer(false);
if (USE_DATA_WATCHER) {
dataWatcher.setObject(new WrappedDataWatcher.WrappedDataWatcherObject(ITEM_INDEX, serializer), item);
} else {
values.add(new WrappedDataValue(
ITEM_INDEX,
serializer,
BukkitConverters.getItemStackConverter().getGeneric(item)
));
values.add(new WrappedDataValue(ITEM_INDEX, serializer,
BukkitConverters.getItemStackConverter().getGeneric(item)));
}
return this;
}
public @NotNull EntityMetadataPacket setRotation(@NotNull Rotation rotation) {
WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.get(Integer.class);
final WrappedDataWatcher.Serializer serializer = WrappedDataWatcher.Registry.get(Integer.class);
if (USE_DATA_WATCHER) {
dataWatcher.setObject(
new WrappedDataWatcher.WrappedDataWatcherObject(ROTATION_INDEX, serializer),
rotation.ordinal()
);
dataWatcher.setObject(new WrappedDataWatcher.WrappedDataWatcherObject(ROTATION_INDEX, serializer),
rotation.ordinal());
} else {
values.add(new WrappedDataValue(ROTATION_INDEX, serializer, rotation.ordinal()));
}
return this;
}
public @NotNull EntityMetadataPacket build() {
if (USE_DATA_WATCHER) {
getWatchableCollectionModifier().write(0, dataWatcher.getWatchableObjects());
} else {
getDataValueCollectionModifier().write(0, values);
}
return this;
}
}
}
@@ -1,73 +1,98 @@
package io.josemmo.bukkit.plugin.packets;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier;
import io.josemmo.bukkit.plugin.utils.Internals;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class MapDataPacket extends PacketContainer {
private static final long serialVersionUID = 1L;
private static final int LOCKED_INDEX;
private @Nullable StructureModifier<?> mapDataModifier;
static {
LOCKED_INDEX = (Internals.MINECRAFT_VERSION < 17) ? 1 : 0;
}
public MapDataPacket() {
super(PacketType.Play.Server.MAP);
getModifier().writeDefaults();
if (Internals.MINECRAFT_VERSION < 17) {
// Disable tracking position
getBooleans().write(0, false);
} else {
// Create modifier for map data instance
Class<?> mapDataType = getModifier().getField(4).getType();
Object mapDataInstance = getModifier().read(4);
final Class<?> mapDataType = getModifier().getField(4).getType();
final Object mapDataInstance = getModifier().read(4);
mapDataModifier = new StructureModifier<>(mapDataType).withTarget(mapDataInstance);
}
}
public @NotNull MapDataPacket setId(int id) {
getIntegers().write(0, id);
return this;
}
public @NotNull MapDataPacket setArea(int columns, int rows, int x, int z) {
if (mapDataModifier == null) {
getIntegers()
.write(1, x)
.write(2, z)
.write(3, columns)
.write (4, rows);
getIntegers().write(1, x).write(2, z).write(3, columns).write(4, rows);
} else {
mapDataModifier.withType(Integer.TYPE)
.write(0, x)
.write(1, z)
.write(2, columns)
.write(3, rows);
mapDataModifier.withType(Integer.TYPE).write(0, x).write(1, z).write(2, columns).write(3, rows);
}
return this;
}
public @NotNull MapDataPacket setScale(int scale) {
getBytes().write(0, (byte) scale);
return this;
}
public @NotNull MapDataPacket setLocked(boolean locked) {
getBooleans().write(LOCKED_INDEX, locked);
return this;
}
public @NotNull MapDataPacket setPixels(byte[] pixels) {
if (mapDataModifier == null) {
getByteArrays().write(0, pixels);
} else {
mapDataModifier.withType(byte[].class).write(0, pixels);
}
return this;
}
}
}
@@ -1,69 +1,87 @@
package io.josemmo.bukkit.plugin.packets;
import java.util.UUID;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import com.comphenix.protocol.PacketType;
import com.comphenix.protocol.events.PacketContainer;
import io.josemmo.bukkit.plugin.utils.Internals;
import org.bukkit.entity.EntityType;
import org.jetbrains.annotations.NotNull;
import java.util.UUID;
public class SpawnEntityPacket extends PacketContainer {
private static final long serialVersionUID = 1L;
private static final boolean ROTATION_AS_BYTES;
private static final int DATA_INDEX;
static {
if (Internals.MINECRAFT_VERSION < 19) {
ROTATION_AS_BYTES = false;
DATA_INDEX = 6;
} else {
DATA_INDEX = 4;
ROTATION_AS_BYTES = true;
}
}
public SpawnEntityPacket() {
super(PacketType.Play.Server.SPAWN_ENTITY);
getIntegers()
.write(1, 0)
.write(2, 0)
.write(3, 0);
getUUIDs()
.write(0, UUID.randomUUID());
getIntegers().write(1, 0).write(2, 0).write(3, 0);
getUUIDs().write(0, UUID.randomUUID());
}
public @NotNull SpawnEntityPacket setEntityType(@NotNull EntityType type) {
getEntityTypeModifier().write(0, type);
return this;
}
public @NotNull SpawnEntityPacket setId(int id) {
getIntegers().write(0, id);
return this;
}
public @NotNull SpawnEntityPacket setRotation(int pitch, int yaw) {
if (ROTATION_AS_BYTES) {
getBytes()
.write(0, (byte) pitch)
.write(1, (byte) yaw);
getBytes().write(0, (byte) pitch).write(1, (byte) yaw);
} else {
getIntegers()
.write(4, pitch)
.write(5, yaw);
getIntegers().write(4, pitch).write(5, yaw);
}
return this;
}
public @NotNull SpawnEntityPacket setData(int data) {
getIntegers().write(DATA_INDEX, data);
return this;
}
public @NotNull SpawnEntityPacket setPosition(double x, double y, double z) {
getDoubles()
.write(0, x)
.write(1, y)
.write(2, z);
getDoubles().write(0, x).write(1, y).write(2, z);
return this;
}
}
}
@@ -8,33 +8,46 @@ 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,82 +56,126 @@ 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;
}
int retry = 0;
while (true) {
try {
WrappedDataWatcher.Registry.get(Byte.class);
READY = true;
ready = true;
break;
} catch (Exception e) {
if (++retry > 20) {
// Exhausted max. retries
throw e;
}
tryToSleep(200);
}
}
}
/**
* Try to sleep
* <p>
* NOTE: Will wait synchronously, blocking the invoker thread
*
* @param ms Delay in milliseconds
*/
protected static void tryToSleep(long ms) {
try {
Thread.sleep(ms);
} catch (Exception __) {
// Fail silently
}
}
/**
* Try to send packet
*
* @param player Player who will receive the packet
* @param packet Packet to send
*/
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);
}
}
/**
* Try to send several packets
*
* @param player Player who will receive the packets
* @param packets Packets to send
*/
protected static void tryToSendPackets(@NotNull Player player, @NotNull Iterable<PacketContainer> packets) {
if (Internals.MINECRAFT_VERSION < 19.4f) {
for (PacketContainer packet : packets) {
tryToSendPacket(player, packet);
}
} else {
PacketContainer container = new PacketContainer(PacketType.Play.Server.BUNDLE);
container.getPacketBundles().write(0, packets);
tryToSendPacket(player, container);
}
}
/**
* Try to run asynchronous task
*
* @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,8 @@ 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,33 +46,61 @@ 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;
private FakeItemFrame[] frames = null;
private int delay = 0; // Delay between steps in 50ms intervals, "0" for N/A
private int numOfSteps = -1; // Total number of animation steps
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
* @param location Player eye location
* @return Image rotation
*
* @param face Image block face
* @param location Player eye location
* @return Image rotation
*/
public static @NotNull Rotation getRotationFromPlayerEyesight(@NotNull BlockFace face, @NotNull Location location) {
// Images placed on N/S/E/W faces never have rotation
if (face != BlockFace.UP && face != BlockFace.DOWN) {
return Rotation.NONE;
}
// Top and down images depend on where player is looking
BlockFace eyeDirection = DirectionUtils.getCardinalDirection(location.getYaw());
switch (eyeDirection) {
case EAST:
return (face == BlockFace.DOWN) ? Rotation.CLOCKWISE_135 : Rotation.CLOCKWISE_45;
case SOUTH:
@@ -87,65 +109,45 @@ public class FakeImage extends FakeEntity {
return (face == BlockFace.DOWN) ? Rotation.CLOCKWISE_45 : Rotation.CLOCKWISE_135;
default:
return Rotation.NONE;
}
}
/**
* 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)
*
* @param sizeInPixels Image file dimension in pixels
* @param width Desired width in blocks
* @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;
}
/**
* Class constructor
* @param filename Image filename
* @param location Top-left corner where image will be placed
* @param face Block face
* @param rotation Image rotation
* @param width Width in blocks
* @param height Height in blocks
* @param placedAt Placed at
* @param placedBy Placed by
* @param flags Flags
*
* @param filename Image filename
* @param location Top-left corner where image will be placed
* @param face Block face
* @param rotation Image rotation
* @param width Width in blocks
* @param height Height in blocks
* @param placedAt Placed at
* @param placedBy Placed by
* @param flags Flags
*/
public FakeImage(
@NotNull String filename,
@NotNull Location location,
@NotNull BlockFace face,
@NotNull Rotation rotation,
int width,
int height,
@Nullable Date placedAt,
@NotNull OfflinePlayer placedBy,
int flags
) {
public FakeImage(@NotNull String filename, @NotNull Location location, @NotNull BlockFace face,
@NotNull Rotation rotation, int width, int height, @Nullable Date placedAt, @NotNull OfflinePlayer placedBy,
int flags)
{
this.filename = filename;
this.location = location;
this.face = face;
@@ -156,364 +158,503 @@ public class FakeImage extends FakeEntity {
this.placedBy = placedBy;
this.flags = flags;
// Define function for retrieving item frame positional vector from <row,column> pair
// Define function for retrieving item frame positional vector from <row,column>
// pair
if (face == BlockFace.SOUTH) {
getLocationVector = (col, row) -> new Vector(col, -row, 0);
} else if (face == BlockFace.NORTH) {
getLocationVector = (col, row) -> new Vector(-col, -row, 0);
} else if (face == BlockFace.EAST) {
getLocationVector = (col, row) -> new Vector(0, -row, -col);
} else if (face == BlockFace.WEST) {
getLocationVector = (col, row) -> new Vector(0, -row, col);
} else if (face == BlockFace.UP) {
if (rotation == Rotation.CLOCKWISE_45) {
getLocationVector = (col, row) -> new Vector(-row, 0, col);
} else if (rotation == Rotation.CLOCKWISE_135) {
getLocationVector = (col, row) -> new Vector(row, 0, -col);
} else if (rotation == Rotation.CLOCKWISE) {
getLocationVector = (col, row) -> new Vector(-col, 0, -row);
} else { // Rotation.NONE
getLocationVector = (col, row) -> new Vector(col, 0, row);
}
} else { // BlockFace.DOWN
if (rotation == Rotation.CLOCKWISE_45) {
getLocationVector = (col, row) -> new Vector(-row, 0, -col);
} else if (rotation == Rotation.CLOCKWISE_135) {
getLocationVector = (col, row) -> new Vector(row, 0, col);
} else if (rotation == Rotation.CLOCKWISE) {
getLocationVector = (col, row) -> new Vector(-col, 0, row);
} else { // Rotation.NONE
getLocationVector = (col, row) -> new Vector(col, 0, -row);
}
}
LOGGER.fine("Created FakeImage#(" + location + "," + face + ") from ImageFile#(" + filename + ")");
plugin.fine("Created FakeImage#(" + location + "," + face + ") from ImageFile#(" + filename + ")");
}
/**
* Get image filename
*
* @return Image filename
*/
public @NotNull String getFilename() {
return filename;
}
/**
* Get image file instance
*
* @return Image file instance or NULL if not found
*/
public @Nullable ImageFile getFile() {
return YamipaPlugin.getInstance().getStorage().get(filename);
return plugin.getStorage().get(filename);
}
/**
* Get top-left corner of image
*
* @return Location instance
*/
public @NotNull Location getLocation() {
return location;
}
/**
* Get image block face
*
* @return Image block face
*/
public @NotNull BlockFace getBlockFace() {
return face;
}
/**
* Get image rotation
*
* @return Image rotation
*/
public @NotNull Rotation getRotation() {
return rotation;
}
/**
* Get image width in blocks
*
* @return Image width
*/
public int getWidth() {
return width;
}
/**
* Get image height in blocks
*
* @return Image height
*/
public int getHeight() {
return height;
}
/**
* Get placed at date
*
* @return Placed at date
*/
public @Nullable Date getPlacedAt() {
return placedAt;
}
/**
* Get placed by player
*
* @return Placed by player instance
*/
public @NotNull OfflinePlayer getPlacedBy() {
return placedBy;
}
/**
* Get flags
*
* @return Flags
*/
public int getFlags() {
return flags;
}
/**
* Has flag
* @param flag Flag to check
* @return Whether instance has given flag or not
*
* @param flag Flag to check
* @return Whether instance has given flag or not
*/
public boolean hasFlag(int flag) {
return ((flags & flag) == flag);
}
/**
* Get image delay
*
* @return Image delay in 50ms intervals
*/
public int getDelay() {
return delay;
}
/**
* Get all block locations covered by the image
*
* @return Array of location instances
*/
public @NotNull Location[] getAllLocations() {
Location[] locations = new Location[width*height];
for (int row=0; row<height; row++) {
for (int col=0; col<width; col++) {
locations[row*width + col] = location.clone().add(getLocationVector.apply(col, row));
Location[] locations = new Location[width * height];
for (int row = 0; row < height; row++) {
for (int col = 0; col < width; col++) {
locations[row * width + col] = location.clone().add(getLocationVector.apply(col, row));
}
}
return locations;
}
/**
* Get the world area IDs where this image is located
*
* @return Array of world area IDs
*/
public @NotNull WorldAreaId[] getWorldAreaIds() {
Set<WorldAreaId> corners = new HashSet<>();
corners.add(WorldAreaId.fromLocation(location));
corners.add(WorldAreaId.fromLocation(location.clone().add(getLocationVector.apply(width-1, 0))));
corners.add(WorldAreaId.fromLocation(location.clone().add(getLocationVector.apply(0, height-1))));
corners.add(WorldAreaId.fromLocation(location.clone().add(getLocationVector.apply(width-1, height-1))));
corners.add(WorldAreaId.fromLocation(location.clone().add(getLocationVector.apply(width - 1, 0))));
corners.add(WorldAreaId.fromLocation(location.clone().add(getLocationVector.apply(0, height - 1))));
corners.add(WorldAreaId.fromLocation(location.clone().add(getLocationVector.apply(width - 1, height - 1))));
return corners.toArray(new WorldAreaId[0]);
}
/**
* Verify whether a point is contained in the image plane
* @param location Location instance (only for coordinates)
* @param face Block face
* @return TRUE for contained, FALSE otherwise
*
* @param location Location instance (only for coordinates)
* @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) {
return false;
}
// Get sorted plane edges
Location topLeft = this.location;
Location bottomRight = this.location.clone().add(getLocationVector.apply(width-1, height-1));
int[] x = new int[]{topLeft.getBlockX(), bottomRight.getBlockX()};
int[] y = new int[]{topLeft.getBlockY(), bottomRight.getBlockY()};
int[] z = new int[]{topLeft.getBlockZ(), bottomRight.getBlockZ()};
Location bottomRight = this.location.clone().add(getLocationVector.apply(width - 1, height - 1));
int[] x = new int[] { topLeft.getBlockX(), bottomRight.getBlockX() };
int[] y = new int[] { topLeft.getBlockY(), bottomRight.getBlockY() };
int[] z = new int[] { topLeft.getBlockZ(), bottomRight.getBlockZ() };
Arrays.sort(x);
Arrays.sort(y);
Arrays.sort(z);
// Is point located inside the plane limits?
int[] point = new int[] {location.getBlockX(), location.getBlockY(), location.getBlockZ()};
if (point[0] < x[0] || point[0] > x[1]) return false;
if (point[1] < y[0] || point[1] > y[1]) return false;
if (point[2] < z[0] || point[2] > z[1]) return false;
int[] point = new int[] { location.getBlockX(), location.getBlockY(), location.getBlockZ() };
if (point[0] < x[0] || point[0] > x[1])
return false;
if (point[1] < y[0] || point[1] > y[1])
return false;
if (point[2] < z[0] || point[2] > z[1])
return false;
return true;
}
/**
* Set on loaded listener
* <p>
* Defines a single-use listener that gets called when the fake image finishes loading,
* at which point the listener is automatically unregistered.
* Defines a single-use listener that gets called when the fake image finishes
* loading, at which point the listener is automatically unregistered.
*
* @param onLoadedListener Listener
*/
public void setOnLoadedListener(@NotNull Runnable onLoadedListener) {
this.onLoadedListener = onLoadedListener;
}
/**
* Load generated instance attributes
*/
private void load() {
ImageFile file = getFile();
// Get maps to use
FakeMap[][][] maps;
ImageFile file = getFile();
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];
FakeItemFrame[] newFrames = new FakeItemFrame[width * height];
boolean glowing = hasFlag(FLAG_GLOWING);
for (int col=0; col<width; col++) {
for (int row=0; row<height; row++) {
for (int col = 0; col < width; col++) {
for (int row = 0; row < height; row++) {
Location frameLocation = location.clone().add(getLocationVector.apply(col, row));
newFrames[height*col+row] = new FakeItemFrame(frameLocation, face, rotation, glowing, maps[col][row]);
newFrames[height * col + row] = new FakeItemFrame(frameLocation, face, rotation, glowing,
maps[col][row]);
}
}
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) {
task = plugin.getScheduler().scheduleAtFixedRate(
this::nextStep,
0,
delay*50L,
TimeUnit.MILLISECONDS
);
LOGGER.fine("Spawned animation task for FakeImage#(" + location + "," + face + ")");
if (animateImages && task == null && hasFlag(FLAG_ANIMATABLE) && numOfSteps > 1) {
task = plugin.getScheduler().scheduleAtFixedRate(this::nextStep, 0, delay * 50L, TimeUnit.MILLISECONDS);
plugin.fine("Spawned animation task for FakeImage#(" + location + "," + face + ")");
}
// Notify listener
if (onLoadedListener != null) {
onLoadedListener.run();
onLoadedListener = null;
}
}
/**
* Spawn image for a player
*
* @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) {
spawnOnceLoaded(player);
return;
}
// Wait for instance to load if loading
if (loading) {
tryToRunAsyncTask(() -> {
while (loading) {
tryToSleep(5);
}
spawnOnceLoaded(player);
});
return;
}
// Instance needs to be loaded
loading = true;
tryToRunAsyncTask(() -> {
waitForProtocolLib();
load();
loading = false;
spawnOnceLoaded(player);
});
}
/**
* Spawn image for a player (once instance has been loaded)
*
* @param player Player instance
*/
private void spawnOnceLoaded(@NotNull Player player) {
String playerName = player.getName();
observingPlayers.add(player);
// Prepare packets to send
List<PacketContainer> packets = new ArrayList<>();
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
tryToSendPackets(player, packets);
}
/**
* Destroy image for all players
*/
public void destroy() {
if (frames != null) {
destroy(null);
}
}
/**
* Destroy image a player
*
* @param player Player instance or NULL for all observing players
*/
public void destroy(@Nullable Player player) {
LOGGER.fine(
"Received request to destroy FakeImage#(" + location + "," + face + ") for " +
(player == null ? "all players" : "Player#" + player.getName())
);
plugin.fine("Received request to destroy FakeImage#(" + location + "," + face + ") for "
+ (player == null ? "all players" : "Player#" + player.getName()));
// Send packets to destroy item frames
if (frames != null) {
Set<Player> targets = (player == null) ? observingPlayers : Collections.singleton(player);
for (Player target : targets) {
String targetName = target.getName();
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);
}
}
// Remove player from observing players
if (player == null) {
observingPlayers.clear();
} else {
observingPlayers.remove(player);
}
// Invalidate instance (if no more players)
if (observingPlayers.isEmpty()) {
invalidate();
}
}
/**
* Notify player quit from server
*
* @param player Player instance
*/
public void notifyPlayerQuit(@NotNull Player player) {
observingPlayers.remove(player);
if (observingPlayers.isEmpty()) {
invalidate();
}
}
/**
@@ -522,43 +663,59 @@ public class FakeImage extends FakeEntity {
* Removes all item frames associated with this image, among other things.
*/
private void invalidate() {
// Destroy animation task
if (task != null) {
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();
if (file != null) {
file.unsubscribe(this);
}
}
/**
* Send next animation step to all registered players
*/
private void nextStep() {
currentStep = (currentStep + 1) % numOfSteps;
try {
for (Player player : observingPlayers) {
List<PacketContainer> packets = new ArrayList<>();
for (FakeItemFrame frame : frames) {
packets.addAll(frame.getRenderPackets(player, currentStep));
}
tryToSendPackets(player, packets);
}
} catch (ConcurrentModificationException e) {
// We can safely ignore this exception as it will just result
// in a dropped step (all `observingPlayers` modifications are
// called from the same thread).
}
}
}
@@ -1,14 +1,9 @@
package io.josemmo.bukkit.plugin.renderer;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
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 java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Rotation;
@@ -17,16 +12,23 @@ import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.nbt.NbtCompound;
import com.comphenix.protocol.wrappers.nbt.NbtFactory;
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;
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;
@@ -36,54 +38,66 @@ public class FakeItemFrame extends FakeEntity {
/**
* Get next unused item frame ID
*
* @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;
});
}
/**
* Class constructor
*
* @param location Frame location
* @param face Block face
* @param rotation Frame rotation
* @param glowing Whether is glowing or regular frame
* @param maps Fake maps to animate
*/
public FakeItemFrame(
@NotNull Location location,
@NotNull BlockFace face,
@NotNull Rotation rotation,
boolean glowing,
@NotNull FakeMap[] maps
) {
public FakeItemFrame(@NotNull Location location, @NotNull BlockFace face, @NotNull Rotation rotation,
boolean glowing, @NotNull FakeMap[] maps)
{
this.id = getNextId();
this.location = location;
this.face = face;
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)");
}
/**
* Get frame ID
*
* @return Frame ID
*/
public int getId() {
return id;
}
/**
* Get entity spawn packet
*
* @return Spawn packet
*/
public @NotNull SpawnEntityPacket getSpawnPacket() {
// Calculate frame position in relation to target block
double x = location.getBlockX();
double y = location.getBlockY();
@@ -92,6 +106,7 @@ public class FakeItemFrame extends FakeEntity {
int yaw = 0;
int orientation = 3;
switch (face) {
case UP:
++y;
pitch = -64;
@@ -119,58 +134,65 @@ public class FakeItemFrame extends FakeEntity {
break;
case SOUTH:
++z;
default:
break;
}
// Create item frame entity
SpawnEntityPacket framePacket = new SpawnEntityPacket();
final SpawnEntityPacket framePacket = new SpawnEntityPacket();
framePacket.setId(id)
.setEntityType((glowing && SUPPORTS_GLOWING) ? EntityType.GLOW_ITEM_FRAME : EntityType.ITEM_FRAME)
.setPosition(x, y, z)
.setRotation(pitch, yaw)
.setData(orientation);
.setEntityType((glowing && SUPPORTS_GLOWING) ? EntityType.GLOW_ITEM_FRAME : EntityType.ITEM_FRAME)
.setPosition(x, y, z).setRotation(pitch, yaw).setData(orientation);
return framePacket;
}
/**
* Get frame of animation packets
*
* @param player Player who is expected to receive packets (for caching reasons)
* @param step Map step
*/
public @NotNull List<PacketContainer> getRenderPackets(@NotNull Player player, int step) {
List<PacketContainer> packets = new ArrayList<>(2);
final List<PacketContainer> packets = new ArrayList<>(2);
// Enqueue map pixels packet (if needed)
boolean mustSendPixels = maps[step].requestResend(player);
final boolean mustSendPixels = maps[step].requestResend(player);
if (mustSendPixels) {
packets.add(maps[step].getPixelsPacket());
}
// Create and attach filled map
ItemStack itemStack = MinecraftReflection.getBukkitItemStack(new ItemStack(Material.FILLED_MAP));
NbtCompound itemStackNbt = NbtFactory.ofCompound("tag");
final ItemStack itemStack = MinecraftReflection.getBukkitItemStack(new ItemStack(Material.FILLED_MAP));
final NbtCompound itemStackNbt = NbtFactory.ofCompound("tag");
itemStackNbt.put("map", maps[step].getId());
NbtFactory.setItemTag(itemStack, itemStackNbt);
// Build entity metadata packet
EntityMetadataPacket metadataPacket = new EntityMetadataPacket();
metadataPacket.setId(id)
.setInvisible(true)
.setItem(itemStack)
.setRotation(rotation)
.build();
final EntityMetadataPacket metadataPacket = new EntityMetadataPacket();
metadataPacket.setId(id).setInvisible(true).setItem(itemStack).setRotation(rotation).build();
packets.add(metadataPacket);
return packets;
}
/**
* Get destroy item frame packet
*
* @return Destroy packet
*/
public @NotNull DestroyEntityPacket getDestroyPacket() {
DestroyEntityPacket destroyPacket = new DestroyEntityPacket();
final DestroyEntityPacket destroyPacket = new DestroyEntityPacket();
destroyPacket.setId(id);
return destroyPacket;
}
}
}
@@ -1,12 +1,6 @@
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.awt.Color;
import java.time.Instant;
import java.util.Arrays;
import java.util.UUID;
@@ -14,146 +8,195 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicInteger;
import org.bukkit.entity.Player;
import org.bukkit.map.MapPalette;
import org.jetbrains.annotations.NotNull;
import io.josemmo.bukkit.plugin.packets.MapDataPacket;
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<>();
/**
* Get next unused map ID
*
* @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;
});
}
/**
* Pixel to Minecraft color index
* @param pixel RGBA pixel value
* @return Closest Minecraft color index
*
* @param pixel RGBA pixel value
* @return Closest Minecraft color index
*/
@SuppressWarnings("deprecation")
public static byte pixelToIndex(int pixel) {
return MapPalette.matchColor(new Color(pixel, true));
}
/**
* Get map instance to show in case of error
*
* @return Error instance
*/
private static @NotNull FakeMap getErrorInstance() {
if (ERROR_INSTANCE == null) {
byte[] pixels = new byte[DIMENSION * DIMENSION];
if (errorInstance == null) {
final 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
*
* @param width Width in blocks
* @param height Height in blocks
* @return Fake maps container
*/
public static @NotNull FakeMap[][][] getErrorMatrix(int width, int height) {
FakeMap[] errorMaps = new FakeMap[] {getErrorInstance()};
FakeMap[][][] matrix = new FakeMap[width][height][1];
public static @NotNull FakeMapsContainer getErrorMatrix(int width, int height) {
final FakeMap[] errorMaps = new FakeMap[] { getErrorInstance() };
final FakeMap[][][] matrix = new FakeMap[width][height][1];
for (FakeMap[][] column : matrix) {
Arrays.fill(column, errorMaps);
}
return matrix;
return new FakeMapsContainer(matrix, 0);
}
/**
* Class constructor
*
* @param pixels Array of Minecraft color indexes
* @param scanSize Original image width
* @param startX Initial X pixel coordinate
* @param startY Initial Y pixel coordinate
*/
public FakeMap(byte[] pixels, int scanSize, int startX, int startY) {
this.id = getNextId();
// Copy square of pixels to this instance
this.pixels = new byte[DIMENSION*DIMENSION];
for (int y=0; y<DIMENSION; y++) {
System.arraycopy(pixels, startX+(startY+y)*scanSize, this.pixels, y*DIMENSION, DIMENSION);
this.pixels = new byte[DIMENSION * DIMENSION];
for (int y = 0; y < DIMENSION; y++) {
System.arraycopy(pixels, startX + (startY + y) * scanSize, this.pixels, y * DIMENSION, DIMENSION);
}
LOGGER.fine("Created FakeMap#" + this.id);
plugin.fine("Created FakeMap#" + this.id);
}
/**
* Class constructor
*
* @param pixels Array of Minecraft color indexes
*/
public FakeMap(byte[] pixels) {
this.id = getNextId();
this.pixels = pixels;
LOGGER.fine("Created FakeMap#" + this.id);
plugin.fine("Created FakeMap#" + this.id);
}
/**
* Get map ID
*
* @return Map ID
*/
public int getId() {
return id;
}
/**
* Get raw pixels
*
* @return Array of Minecraft color indexes
*/
public byte[] getPixels() {
return pixels;
}
/**
* Request re-send of map pixels
* @param player Player who is expected to receive pixels
* @return Whether re-send authorization was granted or not
*
* @param player Player who is expected to receive pixels
* @return Whether re-send authorization was granted or not
*/
public boolean requestResend(@NotNull Player player) {
UUID uuid = player.getUniqueId();
long now = Instant.now().getEpochSecond();
final UUID uuid = player.getUniqueId();
final long now = Instant.now().getEpochSecond();
// Has enough time passed since last re-send?
long last = lastPlayerSendTime.getOrDefault(uuid, 0L);
if ((now-last) <= RESEND_THRESHOLD && (player.getLastPlayed()/1000) < last) {
final long last = lastPlayerSendTime.getOrDefault(uuid, 0L);
if ((now - last) <= RESEND_THRESHOLD && (player.getLastSeen() / 1000) < last) {
return false;
}
// 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;
}
/**
* Get map pixels packet
*
* @return Map pixels packet
*/
public @NotNull MapDataPacket getPixelsPacket() {
MapDataPacket mapDataPacket = new MapDataPacket();
mapDataPacket.setId(id)
.setScale(0) // Fully zoomed-in
.setLocked(true)
.setArea(DIMENSION, DIMENSION, 0, 0)
.setPixels(pixels);
final MapDataPacket mapDataPacket = new MapDataPacket();
mapDataPacket.setId(id).setScale(0) // Fully zoomed-in
.setLocked(true).setArea(DIMENSION, DIMENSION, 0, 0).setPixels(pixels);
return mapDataPacket;
}
}
}
@@ -0,0 +1,37 @@
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,19 @@ 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,255 +38,299 @@ 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;
}
/**
* Start instance
*/
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);
saveTask = Bukkit.getScheduler().runTaskTimerAsynchronously(plugin, this::saveConfig, SAVE_INTERVAL,
SAVE_INTERVAL);
}
/**
* Stop instance
*/
public void stop() {
HandlerList.unregisterAll(this);
// Destroy images from remote clients
for (Set<FakeImage> fakeImagesPart : images.values()) {
for (FakeImage fakeImage : fakeImagesPart) {
fakeImage.destroy();
}
}
// Persist configuration
if (saveTask != null) {
saveTask.cancel();
}
saveConfig();
// Clear dangling references
images.clear();
imagesCountByPlayer.clear();
playersLocation.clear();
}
/**
* 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;
}
// Try to load configuration
CsvConfiguration config = new CsvConfiguration();
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;
}
// Parse each row
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()) ?
new Date(Long.parseLong(row[9])*1000L) :
null;
UUID placedById = (row.length > 10 && !row[10].isEmpty()) ?
UUID.fromString(row[10]) :
FakeImage.UNKNOWN_PLAYER_ID;
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].equals("")) ? UUID.fromString(row[10])
: FakeImage.UNKNOWN_PLAYER_ID;
OfflinePlayer placedBy = Bukkit.getOfflinePlayer(placedById);
int flags = (row.length > 11) ?
Math.max(Integer.parseInt(row[11]), 0) :
FakeImage.DEFAULT_PLACE_FLAGS;
FakeImage fakeImage = new FakeImage(filename, location, face, rotation, width, height,
placedAt, placedBy, flags);
int flags = (row.length > 11) ? Math.max(Integer.parseInt(row[11]), 0) : FakeImage.DEFAULT_PLACE_FLAGS;
FakeImage fakeImage = new FakeImage(filename, location, face, rotation, width, height, 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);
}
}
}
/**
* Save configuration to disk
*/
@SuppressWarnings("ExtractMethodRecommender")
private void saveConfig() {
if (!hasConfigChanged.get()) return;
if (!hasConfigChanged.get())
return;
// Get all fake images
Set<FakeImage> fakeImages = new HashSet<>();
for (Set<FakeImage> fakeImagesPart : images.values()) {
fakeImages.addAll(fakeImagesPart);
}
// Placed here so, if another change comes while saving, we don't lose those changes (will be saved later)
// Placed here so, if another change comes while saving, we don't lose those
// changes (will be saved later)
hasConfigChanged.set(false);
// Export to configuration properties
CsvConfiguration config = new CsvConfiguration();
for (FakeImage fakeImage : fakeImages) {
Location location = fakeImage.getLocation();
UUID placedById = fakeImage.getPlacedBy().getUniqueId();
String[] row = new String[]{
fakeImage.getFilename(),
location.getChunk().getWorld().getName(),
location.getBlockX() + "",
location.getBlockY() + "",
location.getBlockZ() + "",
fakeImage.getBlockFace().name(),
fakeImage.getRotation().name(),
fakeImage.getWidth() + "",
fakeImage.getHeight() + "",
(fakeImage.getPlacedAt() == null) ? "" : (fakeImage.getPlacedAt().getTime() / 1000) + "",
placedById.equals(FakeImage.UNKNOWN_PLAYER_ID) ? "" : placedById.toString(),
fakeImage.getFlags() + ""
};
String[] row = new String[] { fakeImage.getFilename(), location.getChunk().getWorld().getName(),
location.getBlockX() + "", location.getBlockY() + "", location.getBlockZ() + "",
fakeImage.getBlockFace().name(), fakeImage.getRotation().name(), fakeImage.getWidth() + "",
fakeImage.getHeight() + "",
(fakeImage.getPlacedAt() == null) ? "" : (fakeImage.getPlacedAt().getTime() / 1000) + "",
placedById.equals(FakeImage.UNKNOWN_PLAYER_ID) ? "" : placedById.toString(),
fakeImage.getFlags() + "" };
config.addRow(row);
}
// 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);
}
}
/**
* Add image to renderer
*
* @param image Fake image instance
* @param isInit TRUE if called during renderer startup, FALSE otherwise
*/
public void addImage(@NotNull FakeImage image, boolean isInit) {
WorldAreaId[] imageWorldAreaIds = image.getWorldAreaIds();
// 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);
}
// Set configuration changed flag
if (!isInit) {
hasConfigChanged.set(true);
}
// Increment count of placed images by player
UUID placedById = image.getPlacedBy().getUniqueId();
imagesCountByPlayer.compute(placedById, (__, prev) -> (prev == null) ? 1 : prev+1);
imagesCountByPlayer.compute(placedById, (__, prev) -> (prev == null) ? 1 : prev + 1);
// Spawn image in players nearby
for (Player player : getPlayersInViewDistance(imageWorldAreaIds)) {
image.spawn(player);
}
}
/**
* Add image to renderer
*
* @param image Fake image instance
*/
public void addImage(@NotNull FakeImage image) {
addImage(image, false);
}
/**
* Get image from location
* @param location Fake image location
* @param face Fake image block face
* @return Fake image instance or NULL if not found
*
* @param location Fake image location
* @param face Fake image block face
* @return Fake image instance or NULL if not found
*/
public @Nullable FakeImage getImage(@NotNull Location location, @NotNull BlockFace face) {
WorldAreaId worldAreaId = WorldAreaId.fromLocation(location);
Set<FakeImage> candidateImages = images.get(worldAreaId);
if (candidateImages == null) {
return null;
}
// Find first fake image containing the given location
for (FakeImage image : candidateImages) {
if (image.contains(location, face)) {
return image;
}
}
// No match found
return null;
}
/**
* Get images from area
* @param world World instance
* @param minX Minimum X coordinate
* @param maxX Maximum X coordinate
* @param minZ Minimum Z coordinate
* @param maxZ Maximum Z coordinate
* @return Set of found images
*
* @param world World instance
* @param minX Minimum X coordinate
* @param maxX Maximum X coordinate
* @param minZ Minimum Z coordinate
* @param maxZ Maximum Z coordinate
* @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<>();
for (Map.Entry<WorldAreaId, Set<FakeImage>> entry : images.entrySet()) {
if (!entry.getKey().getWorld().getName().equals(world.getName())) continue;
if (!entry.getKey().getWorld().getName().equals(world.getName()))
continue;
for (FakeImage image : entry.getValue()) {
Location loc = image.getLocation();
if (loc.getBlockX() < minX || loc.getBlockX() > maxX) continue;
if (loc.getBlockZ() < minZ || loc.getBlockZ() > maxZ) continue;
if (loc.getBlockX() < minX || loc.getBlockX() > maxX)
continue;
if (loc.getBlockZ() < minZ || loc.getBlockZ() > maxZ)
continue;
response.add(image);
}
}
return response;
}
/**
* Remove image from renderer
*
* @param image Fake image instance
*/
public void removeImage(@NotNull FakeImage image) {
WorldAreaId[] imageWorldAreaIds = image.getWorldAreaIds();
// Destroy image from all players nearby
@@ -295,12 +338,16 @@ public class ImageRenderer implements Listener {
// Remove image from renderer
for (WorldAreaId worldAreaId : imageWorldAreaIds) {
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);
}
}
// Set configuration changed flag
@@ -308,104 +355,141 @@ public class ImageRenderer implements Listener {
// Decrement count of placed images by player
UUID placedById = image.getPlacedBy().getUniqueId();
imagesCountByPlayer.compute(placedById, (__, prev) -> (prev != null && prev > 1) ? prev-1 : null);
imagesCountByPlayer.compute(placedById, (__, prev) -> (prev != null && prev > 1) ? prev - 1 : null);
}
/**
* Get set of players who have placed images
*
* @return Offline players
*/
public @NotNull Set<OfflinePlayer> getPlayersWithPlacedImages() {
return imagesCountByPlayer.keySet().stream().map(Bukkit::getOfflinePlayer).collect(Collectors.toSet());
}
/**
* Get number of placed images
*
* @return Number of placed images
*/
public int size() {
return imagesCountByPlayer.values().stream().reduce(0, Integer::sum);
}
/**
* Get number of placed images grouped by player
* <p>
* NOTE: Response is sorted by image count (descending)
*
* @return Images count by player
*/
public @NotNull Map<OfflinePlayer, Integer> getImagesCountByPlayer() {
List<Map.Entry<UUID, Integer>> sortedEntries = new ArrayList<>(imagesCountByPlayer.entrySet());
sortedEntries.sort(Map.Entry.comparingByValue(Comparator.reverseOrder()));
Map<OfflinePlayer, Integer> sortedMap = new LinkedHashMap<>();
for (Map.Entry<UUID, Integer> entry : sortedEntries) {
sortedMap.put(Bukkit.getOfflinePlayer(entry.getKey()), entry.getValue());
}
return sortedMap;
}
/**
* Get players in view distance of the provided world area IDs
* @param ids World area IDs
* @return Players inside those world areas
*
* @param ids World area IDs
* @return Players inside those world areas
*/
private @NotNull Set<Player> getPlayersInViewDistance(@NotNull WorldAreaId[] ids) {
Set<WorldAreaId> neighborhood = new HashSet<>();
for (WorldAreaId worldAreaId : ids) {
Collections.addAll(neighborhood, worldAreaId.getNeighborhood());
}
Set<Player> players = new HashSet<>();
for (Map.Entry<Player, WorldAreaId> entry : playersLocation.entrySet()) {
if (neighborhood.contains(entry.getValue())) {
players.add(entry.getKey());
}
}
return players;
}
/**
* Get images in view distance from world area ID
* @param worldAreaId World area ID
* @return Set of fake images
*
* @param worldAreaId World area ID
* @return Set of fake images
*/
private @NotNull Set<FakeImage> getImagesInViewDistance(@NotNull WorldAreaId worldAreaId) {
Set<FakeImage> response = new HashSet<>();
for (WorldAreaId target : worldAreaId.getNeighborhood()) {
Set<FakeImage> targetImages = images.get(target);
if (targetImages != null) {
response.addAll(targetImages);
}
}
return response;
}
/**
* On player location change
*
* @param player Player instance
* @param location New player location
*/
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;
}
// Has player moved to another world area?
WorldAreaId worldAreaId = WorldAreaId.fromLocation(location);
WorldAreaId prevWorldAreaId = playersLocation.get(player);
if (worldAreaId.equals(prevWorldAreaId)) {
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);
Set<FakeImage> currentState = (prevWorldAreaId == null) ? new HashSet<>() : getImagesInViewDistance(prevWorldAreaId);
Set<FakeImage> currentState = (prevWorldAreaId == null) ? new HashSet<>()
: getImagesInViewDistance(prevWorldAreaId);
Set<FakeImage> imagesToLoad = new HashSet<>(desiredState);
imagesToLoad.removeAll(currentState);
Set<FakeImage> imagesToUnload = new HashSet<>(currentState);
@@ -413,65 +497,101 @@ public class ImageRenderer implements Listener {
// Spawn/destroy images
for (FakeImage image : imagesToUnload) {
image.destroy(player);
}
for (FakeImage image : imagesToLoad) {
image.spawn(player);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
onPlayerLocationChange(event.getPlayer(), event.getPlayer().getLocation());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
Player player = event.getPlayer();
// Get player's current world area ID
WorldAreaId worldAreaId = playersLocation.get(player);
if (worldAreaId == null) return;
if (worldAreaId == null)
return;
playersLocation.remove(player);
// Notify world areas that player quit
for (FakeImage image : getImagesInViewDistance(worldAreaId)) {
image.notifyPlayerQuit(player);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerRespawn(@NotNull PlayerRespawnEvent event) {
onPlayerLocationChange(event.getPlayer(), event.getPlayer().getLocation());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerTeleport(@NotNull PlayerTeleportEvent event) {
if (event.getTo() == null) return;
if (event.getFrom().getChunk().equals(event.getTo().getChunk())) return;
if (event.getTo() == null)
return;
if (event.getFrom().getChunk().equals(event.getTo().getChunk()))
return;
// 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()));
// This is necessary as teleport events get fired *before* teleporting the
// player
Bukkit.getScheduler().runTask(plugin, () -> {
onPlayerLocationChange(event.getPlayer(), event.getTo());
});
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerMove(@NotNull PlayerMoveEvent event) {
if (event.getTo() == null) return;
if (event.getFrom().getChunk().equals(event.getTo().getChunk())) return;
if (event.getTo() == null)
return;
if (event.getFrom().getChunk().equals(event.getTo().getChunk()))
return;
onPlayerLocationChange(event.getPlayer(), event.getTo());
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onVehicleMove(@NotNull VehicleMoveEvent event) {
List<Entity> passengers = event.getVehicle().getPassengers();
if (passengers.isEmpty()) return;
if (event.getFrom().getChunk().equals(event.getTo().getChunk())) return;
if (passengers.isEmpty())
return;
if (event.getFrom().getChunk().equals(event.getTo().getChunk()))
return;
for (Entity passenger : passengers) {
if (passenger instanceof Player) {
onPlayerLocationChange((Player) passenger, event.getTo());
}
}
}
}
@@ -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;
@@ -26,87 +25,100 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collections;
import java.util.Objects;
import net.trueog.utilitiesog.UtilitiesOG;
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
* @param image Image file
* @param amount Stack amount
* @param width Image width in blocks
* @param height Image height in blocks
* @param flags Image flags
* @return Image item
*
* @param image Image file
* @param amount Stack amount
* @param width Image width in blocks
* @param height Image height in blocks
* @param flags Image flags
* @return Image item
*/
public static @NotNull ItemStack getImageItem(@NotNull ImageFile image, int amount, int width, int height, int flags) {
public static @NotNull ItemStack getImageItem(@NotNull ImageFile image, int amount, int width, int height,
int flags)
{
ItemStack itemStack = new ItemStack(Material.ITEM_FRAME, amount);
ItemMeta itemMeta = Objects.requireNonNull(itemStack.getItemMeta());
// Set metadata
PersistentDataContainer itemData = itemMeta.getPersistentDataContainer();
itemMeta.setDisplayName(image.getFilename() + ChatColor.AQUA + " (" + width + "x" + height + ")");
itemMeta.setLore(Collections.singletonList("Yamipa image"));
itemData.set(NSK_FILENAME, PersistentDataType.STRING, image.getFilename());
itemMeta.displayName(UtilitiesOG.trueogColorize(image.getName() + "&b (" + width + "x" + height + ")"));
itemMeta.lore(Collections.singletonList(UtilitiesOG.trueogColorize("Yamipa image")));
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);
itemStack.setItemMeta(itemMeta);
return itemStack;
}
/**
* Start service
*/
public void start() {
register();
YamipaPlugin plugin = YamipaPlugin.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
/**
* Stop service
*/
public void stop() {
unregister();
HandlerList.unregisterAll(this);
}
@EventHandler(priority = EventPriority.LOWEST)
public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
CraftingInventory inventory = event.getInventory();
for (@Nullable ItemStack item : inventory.getMatrix()) {
if (item == null) continue;
for (@Nullable
ItemStack item : inventory.getMatrix()) {
if (item == null)
continue;
// Get metadata from item
ItemMeta itemMeta = item.getItemMeta();
if (itemMeta == null) continue;
if (itemMeta == null)
continue;
// Prevent crafting recipes with image items
if (itemMeta.getPersistentDataContainer().has(NSK_FILENAME, PersistentDataType.STRING)) {
inventory.setResult(new ItemStack(Material.AIR));
break;
}
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOWEST)
public void onPlaceItem(@NotNull HangingPlaceEvent event) {
Player player = event.getPlayer();
if (player == null) return;
if (player == null)
return;
PlayerInventory inventory = player.getInventory();
// Prepare variables to hold item metadata
@@ -118,40 +130,52 @@ public class ItemService extends InteractWithEntityListener implements Listener
Integer flags = null;
// Find placed item stack in player's inventory
// NOTE: we cannot use `event.getItemStack()` as it's only supported since 1.17.1
for (int i=0; i<2; i++) {
// NOTE: we cannot use `event.getItemStack()` as it's only supported since
// 1.17.1
for (int i = 0; i < 2; i++) {
isOffHand = (i == 1);
item = isOffHand ? inventory.getItemInOffHand() : inventory.getItemInMainHand();
// Get item metadata
ItemMeta itemMeta = item.getItemMeta();
if (itemMeta == null) continue;
if (itemMeta == null)
continue;
PersistentDataContainer itemData = itemMeta.getPersistentDataContainer();
filename = itemData.get(NSK_FILENAME, PersistentDataType.STRING);
if (filename == null) continue;
if (filename == null)
continue;
// Found item, parse metadata
width = itemData.get(NSK_WIDTH, PersistentDataType.INTEGER);
height = itemData.get(NSK_HEIGHT, PersistentDataType.INTEGER);
flags = itemData.get(NSK_FLAGS, PersistentDataType.INTEGER);
break;
}
// Validate item and metadata
if (filename == null) {
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)");
ActionBar.send(player, ChatColor.RED + "Image file \"" + filename + "\" no longer exists");
plugin.warning(player + " tried to place corrupted image item (\"" + filename + "\" no longer exists)");
ActionBar.send(player, "&cImage file \"" + filename + "\" no longer exists");
return;
}
// Prevent item frame placing
@@ -159,74 +183,106 @@ public class ItemService extends InteractWithEntityListener implements Listener
// Validate player permissions
if (!player.hasPermission("yamipa.item.place")) {
ActionBar.send(player, ChatColor.RED + "You're not allowed to place image items!");
ActionBar.send(player, "&cYou're not allowed to place image items!");
return;
}
// Try to place image in world
Location location = event.getBlock().getLocation();
boolean success = ImageCommand.placeImage(player, image, width, height, flags, location, event.getBlockFace());
if (!success) return;
if (!success)
return;
// Decrement item from player's inventory
if (player.getGameMode() != GameMode.CREATIVE) {
int amount = item.getAmount();
if (amount > 1) {
item.setAmount(amount - 1);
} else {
item = new ItemStack(Material.AIR);
}
if (isOffHand) {
inventory.setItemInOffHand(item);
} else {
inventory.setItemInMainHand(item);
}
}
}
@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?
FakeImage image = renderer.getImage(location, face);
if (image == null || !image.hasFlag(FakeImage.FLAG_REMOVABLE)) return true;
if (image == null || !image.hasFlag(FakeImage.FLAG_REMOVABLE))
return true;
// Validate player permissions
if (!player.hasPermission("yamipa.item.remove.own")) {
ActionBar.send(player, ChatColor.RED + "You're not allowed to remove image items!");
ActionBar.send(player, "&cYou're not allowed to remove image items!");
return true;
}
if (
!player.getUniqueId().equals(image.getPlacedBy().getUniqueId()) &&
!player.hasPermission("yamipa.item.remove")
) {
ActionBar.send(player, ChatColor.RED + "You cannot remove image items from other players!");
if (!player.getUniqueId().equals(image.getPlacedBy().getUniqueId())
&& !player.hasPermission("yamipa.item.remove"))
{
ActionBar.send(player, "&cYou cannot remove image items from other players!");
return true;
}
// Attempt to remove image
boolean success = ImageCommand.removeImage(player, image);
if (!success) {
return true;
}
// Drop image item
if (player.getGameMode() == GameMode.SURVIVAL && image.hasFlag(FakeImage.FLAG_DROPPABLE)) {
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;
}
@Override
public boolean onInteract(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
// Intentionally left blank
return true;
}
}
@@ -14,6 +14,7 @@ import java.util.Objects;
* World Area IDs represent groups of 16 chunks arranged in a 4x4 square.
*/
public class WorldAreaId {
private static boolean USE_WORLD_VIEW_DISTANCE = true;
private static final Map<String, Integer> SIZES_PER_WORLD = new HashMap<>();
private final World world;
@@ -22,395 +23,246 @@ public class WorldAreaId {
private WorldAreaId[] neighborhood = null;
static {
try {
World.class.getMethod("getViewDistance");
} catch (Exception e) {
USE_WORLD_VIEW_DISTANCE = false;
}
}
/**
* Get ID from location
* @param location Location instance
* @return World area ID
*
* @param location Location instance
* @return World area ID
*/
public static @NotNull WorldAreaId fromLocation(@NotNull Location location) {
Chunk chunk = location.getChunk();
return new WorldAreaId(chunk.getWorld(), chunk.getX() >> 2, chunk.getZ() >> 2);
}
/**
* Class constructor
*
* @param world World instance
* @param x World area X coordinate
* @param z World area Z coordinate
*/
public WorldAreaId(@NotNull World world, int x, int z) {
this.world = world;
this.x = x;
this.z = z;
}
/**
* Get world instance
*
* @return World instance
*/
public @NotNull World getWorld() {
return world;
}
/**
* 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)
if (neighborhood != null) {
return neighborhood;
}
// Calculate neighborhood size from world's view distance
// NOTE: World size is cached to prevent issues with plugins that modify it at runtime
// NOTE: World size is cached to prevent issues with plugins that modify it at
// runtime
int size = SIZES_PER_WORLD.computeIfAbsent(world.getName(), (__) -> {
int distance = USE_WORLD_VIEW_DISTANCE ? world.getViewDistance() : Bukkit.getServer().getViewDistance();
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),
new WorldAreaId(world, x, z-4),
new WorldAreaId(world, x+1, z-4),
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),
neighborhood = new WorldAreaId[] { new WorldAreaId(world, x - 1, z - 4), new WorldAreaId(world, x, z - 4),
new WorldAreaId(world, x + 1, z - 4),
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 - 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-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 - 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),
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 - 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-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 - 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-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 + 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-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 - 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-1, z+4),
new WorldAreaId(world, x, z+4),
new WorldAreaId(world, x+1, z+4),
};
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 - 1, z + 4), new WorldAreaId(world, x, z + 4),
new WorldAreaId(world, x + 1, z + 4), };
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···
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),
// Size 2 (12-chunks radius)
// ··XXX··
// ·XXXXX·
// XXXXXXX
// XXXOXXX
// XXXXXXX
// ·XXXXX·
// ··XXX··
if (size == 2) {
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),
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-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 - 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-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 - 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-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 - 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-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 - 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-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 - 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-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 - 1, z + 3), new WorldAreaId(world, x, z + 3),
new WorldAreaId(world, x + 1, z + 3), };
return neighborhood;
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),
//// Size 1 (8-chunks radius)
// ·XXX·
// XXXXX
// XXOXX
// XXXXX
// ·XXX·
if (size == 1) {
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),
};
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, z - 1),
new WorldAreaId(world, x - 1, z), this, new WorldAreaId(world, x + 1, z),
new WorldAreaId(world, x, z + 1), };
return neighborhood;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
WorldAreaId that = (WorldAreaId) o;
return x == that.x && z == that.z && world.getName().equals(that.world.getName());
}
@Override
public int hashCode() {
return Objects.hash(world, x, z);
}
@Override
public @NotNull String toString() {
return world.getName() + "," + x + "," + z;
}
}
@@ -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,255 @@
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 +258,257 @@ 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);
}
subscribers.computeIfAbsent(cacheKey, __ -> new HashSet<>()).add(subscriber);
lock.unlock();
locks.remove(cacheKey);
return 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());
}
}
}
}
}
/**
* 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
if (!subscribers.containsKey(cacheKey))
return;
}
// Remove subscriber
Set<FakeImage> currentSubscribers = subscribers.get(cacheKey);
@@ -119,21 +516,48 @@ public class ImageFile extends SynchronizedFile {
// Can we clear cached maps?
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.
* This way, next time an image is requested to be rendered, maps will be regenerated.
* 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,210 +1,216 @@
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;
}
/**
* 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");
}
if (cachePath.toFile().mkdirs()) {
LOGGER.info("Created cache 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");
}
// Start file system watcher
super.start();
LOGGER.fine("Found " + files.size() + " file(s) in images directory");
if (new File(cachePath).mkdirs()) {
plugin.info("Created cache directory as it did not exist");
}
// 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);
}
}
}
/**
* Get number of images
*
* @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;
}
public synchronized @NotNull String[] getAllFilenames() {
/**
* 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);
}
return cachedImages.keySet().toArray(new String[0]);
/**
* 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;
}
}
/**
* Get image by filename
* @param filename Filename
* @return Image instance or NULL if not found
*
* @param filename Filename
* @return Image instance or NULL if not found
*/
public synchronized @Nullable ImageFile get(@NotNull String filename) {
return files.get(filename);
return cachedImages.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 + "\"");
}
}
}
@@ -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,96 +7,132 @@ 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;
private BukkitTask task = null;
private ActionBar(@NotNull Player player, @NotNull String message) {
this.player = player;
this.message = message;
}
/**
* Send action bar message
*
* @param player Player who will receive the message
* @param message Message to send
*/
public static void send(@NotNull Player player, @NotNull String message) {
ActionBar instance = new ActionBar(player, message);
instance.sendOnce();
}
/**
* Keep sending action bar message
* @param player Player who will receive the message
* @param message Message to send
* @return New instance
*
* @param player Player who will receive the message
* @param message Message to send
* @return New instance
*/
public static @NotNull ActionBar repeat(@NotNull Player player, @NotNull String message) {
ActionBar instance = new ActionBar(player, message);
instance.start();
return instance;
}
/**
* Set message
* @param message Message
* @return This instance
*
* @param message Message
* @return This instance
*/
public ActionBar setMessage(@NotNull String message) {
this.message = message;
return sendOnce();
}
/**
* Clear message
*
* @return This instance
*/
public ActionBar clear() {
message = "";
stop();
return sendOnce();
}
/**
* Send message once
*
* @return This instance
*/
public ActionBar sendOnce() {
ActionBarPacket actionBarPacket = new ActionBarPacket();
actionBarPacket.setText(message);
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;
}
/**
* Start sending message indefinitely
*
* @return This instance
*/
public ActionBar start() {
if (task == null) {
task = Bukkit.getScheduler().runTaskTimer(plugin, this::sendOnce, 0L, 40L);
}
return this;
}
/**
* Stop sending message indefinitely
*
* @return This instance
*/
public ActionBar stop() {
if (task != null) {
task.cancel();
task = null;
}
return this;
}
}
@@ -1,78 +1,90 @@
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.
*/
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
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<>();
/**
* Get rows
*
* @return List of rows
*/
public @NotNull List<String[]> getRows() {
return data;
}
/**
* Add row
*
* @param row Row
*/
public void addRow(@NotNull String[] row) {
data.add(row);
}
/**
* Load from file
*
* @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)) {
public void load(@NotNull String path) throws IOException {
try (Stream<String> stream = Files.lines(Paths.get(path), CHARSET)) {
stream.forEach(line -> {
line = line.trim();
// Ignore empty lines
if (line.isEmpty()) {
return;
line = StringUtils.trim(line);
if (!StringUtils.isEmpty(line)) {
addRow(line.split(COLUMN_DELIMITER));
}
// Migrate legacy format
if (!line.contains(COLUMN_DELIMITER)) {
line = line.replaceAll("/", COLUMN_DELIMITER);
}
// Parse line
addRow(line.split(COLUMN_DELIMITER));
});
}
}
/**
* Save to file
*
* @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");
}
}
}
}
}
@@ -4,24 +4,41 @@ import org.bukkit.block.BlockFace;
import org.jetbrains.annotations.NotNull;
public class DirectionUtils {
/**
* Get cardinal direction
* @param yaw Yaw angle
* @return Cardinal direction
*
* @param yaw Yaw angle
* @return Cardinal direction
*/
public static @NotNull BlockFace getCardinalDirection(float yaw) {
if (yaw < 0) {
yaw += 360;
}
if (yaw >= 315 || yaw < 45) {
return BlockFace.SOUTH;
} else if (yaw < 135) {
return BlockFace.WEST;
} else if (yaw < 225) {
return BlockFace.NORTH;
} else if (yaw < 315) {
return BlockFace.EAST;
}
return BlockFace.NORTH;
}
}
@@ -14,112 +14,148 @@ 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
* @param player Initiating player
* @param block Target block
* @param face Target block face
* @return Whether to allow original event (<code>true</code>) or not (<code>false</code>)
*
* @param player Initiating player
* @param block Target block
* @param face Target block face
* @return Whether to allow original event (<code>true</code>) or not
* (<code>false</code>)
*/
public abstract boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face);
/**
* On player interact listener
* @param player Initiating player
* @param block Target block
* @param face Target block face
* @return Whether to allow original event (<code>true</code>) or not (<code>false</code>)
*
* @param player Initiating player
* @param block Target block
* @param face Target block face
* @return Whether to allow original event (<code>true</code>) or not
* (<code>false</code>)
*/
public abstract boolean onInteract(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face);
/**
* Get listener priority
*
* @return Listener priority
*/
public @NotNull ListenerPriority getPriority() {
return ListenerPriority.LOWEST;
}
/**
* Register listener
*/
public void register() {
ProtocolLibrary.getProtocolManager().addPacketListener(this);
}
/**
* Unregister listener
*/
public void unregister() {
ProtocolLibrary.getProtocolManager().removePacketListener(this);
}
@Override
public final void onPacketReceiving(@NotNull PacketEvent event) {
Player player = event.getPlayer();
// Discard out-of-range packets
List<Block> lastTwoTargetBlocks = player.getLastTwoTargetBlocks(null, MAX_BLOCK_DISTANCE);
if (lastTwoTargetBlocks.size() != 2) return;
if (lastTwoTargetBlocks.size() != 2)
return;
Block targetBlock = lastTwoTargetBlocks.get(1);
if (!targetBlock.getType().isSolid()) return;
if (!targetBlock.getType().isSolid())
return;
// Get target block face
Block adjacentBlock = lastTwoTargetBlocks.get(0);
BlockFace targetBlockFace = targetBlock.getFace(adjacentBlock);
if (targetBlockFace == null) return;
if (targetBlockFace == null)
return;
// Get action
EnumWrappers.EntityUseAction action;
if (Internals.MINECRAFT_VERSION < 17) {
action = event.getPacket().getEntityUseActions().read(0);
} else {
action = event.getPacket().getEnumEntityUseActions().read(0).getAction();
}
// Notify handler synchronously
boolean allowEvent = true;
try {
if (action == EnumWrappers.EntityUseAction.ATTACK) {
allowEvent = onAttack(player, targetBlock, targetBlockFace);
} else if (action == EnumWrappers.EntityUseAction.INTERACT_AT) {
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)
if (!allowEvent) {
event.setCancelled(true);
}
}
@Override
public final void onPacketSending(PacketEvent event) {
// Intentionally left blank
}
@Override
public final ListeningWhitelist getReceivingWhitelist() {
return ListeningWhitelist.newBuilder()
.priority(getPriority())
.types(PacketType.Play.Client.USE_ENTITY)
.build();
return ListeningWhitelist.newBuilder().priority(getPriority()).types(PacketType.Play.Client.USE_ENTITY).build();
}
@Override
public final ListeningWhitelist getSendingWhitelist() {
return ListeningWhitelist.EMPTY_WHITELIST;
}
@Override
public final Plugin getPlugin() {
return YamipaPlugin.getInstance();
}
}
@@ -6,21 +6,23 @@ 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;
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 {
// Get Minecraft version
String version = Bukkit.getVersion();
version = version.substring(version.lastIndexOf("(MC: 1.")+7, version.length()-1);
version = version.substring(version.lastIndexOf("(MC: 1.") + 7, version.length() - 1);
MINECRAFT_VERSION = Float.parseFloat(version);
// Get "org.bukkit.craftbukkit.CraftServer" references
@@ -31,64 +33,92 @@ 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);
}
}
/**
* Get Brigadier command dispatcher instance
*
* @return Command dispatcher instance
*/
public static @NotNull CommandDispatcher<?> getDispatcher() {
return DISPATCHER;
}
/**
* Get Bukkit command map instance
*
* @return Command map instance
*/
public static @NotNull CommandMap getCommandMap() {
return COMMAND_MAP;
}
/**
* Synchronize Bukkit commands with Brigadier
*/
public static void syncCommands() {
try {
Method syncCommandsMethod = Bukkit.getServer().getClass().getDeclaredMethod("syncCommands");
syncCommandsMethod.setAccessible(true);
syncCommandsMethod.invoke(Bukkit.getServer());
} catch (Exception e) {
throw new RuntimeException("Failed to synchronize Bukkit commands", e);
}
}
/**
* Get Bukkit sender from Brigadier context source
* @param source Brigadier command context source
* @return Command sender instance
*
* @param source Brigadier command context source
* @return Command sender instance
*/
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);
}
}
@@ -1,5 +1,16 @@
package io.josemmo.bukkit.plugin.utils;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import com.palmergames.bukkit.towny.TownyAPI;
import com.palmergames.bukkit.towny.object.TownyPermission;
import com.palmergames.bukkit.towny.utils.PlayerCacheUtil;
@@ -10,184 +21,184 @@ import com.sk89q.worldguard.bukkit.WorldGuardPlugin;
import com.sk89q.worldguard.internal.platform.WorldGuardPlatform;
import com.sk89q.worldguard.protection.flags.Flags;
import com.sk89q.worldguard.protection.flags.StateFlag;
import io.josemmo.bukkit.plugin.YamipaPlugin;
import me.angeschossen.lands.api.LandsIntegration;
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;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
import java.util.concurrent.Callable;
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 __) {
// WorldGuard is not installed
}
try {
griefPrevention = GriefPrevention.instance;
} catch (NoClassDefFoundError __) {
// GriefPrevention is not installed
}
try {
townyApi = TownyAPI.getInstance();
} catch (NoClassDefFoundError __) {
// Towny is not installed
}
try {
landsApi = LandsIntegration.of(YamipaPlugin.getInstance());
} catch (NoClassDefFoundError __) {
// Lands is not installed
}
}
/**
* 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
* @param location Block location
* @return Whether player can build or not
*
* @param player Player instance
* @param location Block location
* @return Whether player can build or not
*/
public static boolean canBuild(@NotNull Player player, @NotNull Location location) {
return queryWorldGuard(player, location, true)
&& queryGriefPrevention(player, location, true)
&& queryTowny(player, location, true)
&& queryLands(player, location, true);
return queryWorldGuard(player, location, true) && queryGriefPrevention(player, location, true)
&& queryTowny(player, location, true) && queryLands(player, location, true);
}
/**
* Can destroy this block
* @param player Player instance
* @param location Block location
* @return Whether player can destroy or not
*
* @param player Player instance
* @param location Block location
* @return Whether player can destroy or not
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public static boolean canDestroy(@NotNull Player player, @NotNull Location location) {
return queryWorldGuard(player, location, false)
&& queryGriefPrevention(player, location, false)
&& queryTowny(player, location, false)
&& queryLands(player, location, false);
return queryWorldGuard(player, location, false) && queryGriefPrevention(player, location, false)
&& queryTowny(player, location, false) && queryLands(player, location, false);
}
private static boolean queryWorldGuard(@NotNull Player player, @NotNull Location location, boolean isBuild) {
if (worldGuard == null) {
return true;
}
WorldGuardPlatform platform = worldGuard.getPlatform();
LocalPlayer wrappedPlayer = WorldGuardPlugin.inst().wrapPlayer(player);
final WorldGuardPlatform platform = worldGuard.getPlatform();
final LocalPlayer wrappedPlayer = WorldGuardPlugin.inst().wrapPlayer(player);
// Grant if bypass permission is enabled
boolean hasBypass = platform.getSessionManager().hasBypass(
wrappedPlayer,
BukkitAdapter.adapt(Objects.requireNonNull(location.getWorld()))
);
final boolean hasBypass = platform.getSessionManager().hasBypass(wrappedPlayer,
BukkitAdapter.adapt(Objects.requireNonNull(location.getWorld())));
if (hasBypass) {
return true;
}
// Check permission (note "BUILD" flag must always be present)
StateFlag flag = isBuild ? Flags.BLOCK_PLACE : Flags.BLOCK_BREAK;
return platform.getRegionContainer().createQuery()
.testState(BukkitAdapter.adapt(location), wrappedPlayer, Flags.BUILD, flag);
final StateFlag flag = isBuild ? Flags.BLOCK_PLACE : Flags.BLOCK_BREAK;
return platform.getRegionContainer().createQuery().testState(BukkitAdapter.adapt(location), wrappedPlayer,
Flags.BUILD, flag);
}
private static boolean queryGriefPrevention(@NotNull Player player, @NotNull Location location, boolean isBuild) {
if (griefPrevention == null) {
return true;
}
YamipaPlugin plugin = YamipaPlugin.getInstance();
final YamipaPlugin plugin = YamipaPlugin.getInstance();
// Build callable depending on permission to check
Callable<Boolean> canEditCallable = isBuild ?
() -> griefPrevention.allowBuild(player, location) == null :
() -> griefPrevention.allowBreak(player, location.getBlock(), location) == null;
final Callable<Boolean> canEditCallable = isBuild ? () -> griefPrevention.allowBuild(player, location) == null
: () -> griefPrevention.allowBreak(player, location.getBlock(), location) == null;
// Check permission from primary thread
try {
return Bukkit.isPrimaryThread() ?
canEditCallable.call() :
Bukkit.getScheduler().callSyncMethod(plugin, canEditCallable).get();
return Bukkit.isPrimaryThread() ? 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;
}
}
private static boolean queryTowny(@NotNull Player player, @NotNull Location location, boolean isBuild) {
if (townyApi == null) {
return true;
}
Material material = location.getBlock().getType();
TownyPermission.ActionType type = isBuild ? TownyPermission.ActionType.BUILD : TownyPermission.ActionType.DESTROY;
final Material material = location.getBlock().getType();
final TownyPermission.ActionType type = isBuild ? TownyPermission.ActionType.BUILD
: TownyPermission.ActionType.DESTROY;
return PlayerCacheUtil.getCachePermission(player, location, material, type);
}
private static boolean queryLands(@NotNull Player player, @NotNull Location location, boolean isBuild) {
if (landsApi == null) {
return true;
}
LandWorld landWorld = landsApi.getWorld(Objects.requireNonNull(location.getWorld()));
final LandWorld landWorld = landsApi.getWorld(Objects.requireNonNull(location.getWorld()));
if (landWorld == null) {
return true;
}
LandPlayer landPlayer = Objects.requireNonNull(landsApi.getLandPlayer(player.getUniqueId()));
RoleFlag flag = isBuild ?
me.angeschossen.lands.api.flags.type.Flags.BLOCK_PLACE :
me.angeschossen.lands.api.flags.type.Flags.BLOCK_BREAK;
final LandPlayer landPlayer = Objects.requireNonNull(landsApi.getLandPlayer(player.getUniqueId()));
final RoleFlag flag = isBuild ? me.angeschossen.lands.api.flags.type.Flags.BLOCK_PLACE
: me.angeschossen.lands.api.flags.type.Flags.BLOCK_BREAK;
return landWorld.hasRoleFlag(landPlayer, location, flag, null, false);
}
}
}
@@ -2,7 +2,6 @@ package io.josemmo.bukkit.plugin.utils;
import com.comphenix.protocol.events.ListenerPriority;
import io.josemmo.bukkit.plugin.YamipaPlugin;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
@@ -20,9 +19,11 @@ import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.BiConsumer;
import net.trueog.utilitiesog.UtilitiesOG;
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;
@@ -32,157 +33,215 @@ public class SelectBlockTask {
/**
* Class constructor
*
* @param player Target player instance
*/
public SelectBlockTask(@NotNull Player player) {
this.player = player;
}
/**
* Set on success callback
*
* @param callback Success callback
*/
public void onSuccess(@Nullable BiConsumer<@NotNull Location, @NotNull BlockFace> callback) {
this.success = callback;
}
/**
* Set on failure (e.g. canceled) callback
*
* @param callback Failure callback
*/
public void onFailure(@Nullable Runnable callback) {
this.failure = callback;
}
/**
* Run task
*
* @param helpMessage Help message for the player
*/
public void run(@NotNull String helpMessage) {
UUID uuid = player.getUniqueId();
// Has this player another active task?
if (instances.containsKey(uuid)) {
player.sendMessage(ChatColor.RED + "You already have a pending action!");
UtilitiesOG.trueogMessage(player, "&cYou already have a pending action!");
return;
}
// Create listener singleton if needed
if (listener == null) {
listener = new SelectBlockTaskListener();
listener.register();
LOGGER.fine("Created SelectBlockTaskListener singleton");
plugin.fine("Created SelectBlockTaskListener singleton");
}
// Start task
instances.put(uuid, this);
actionBar = ActionBar.repeat(player, ChatColor.GREEN + helpMessage + ChatColor.RESET +
" - " + ChatColor.RED + "Left click to cancel");
actionBar = ActionBar.repeat(player, "&a" + helpMessage + "&r - &cLeft click to cancel");
}
/**
* Cancel task
*/
public void cancel() {
if (actionBar != null) {
actionBar.clear();
actionBar = null;
}
instances.remove(player.getUniqueId());
// Destroy listener singleton if no more active tasks
if (instances.isEmpty()) {
listener.unregister();
listener = null;
LOGGER.fine("Destroyed SelectBlockTaskListener singleton");
plugin.fine("Destroyed SelectBlockTaskListener singleton");
}
}
/**
* Internal listener for handling player events
*/
private static class SelectBlockTaskListener extends InteractWithEntityListener implements Listener {
@Override
public void register() {
super.register();
YamipaPlugin plugin = YamipaPlugin.getInstance();
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public void unregister() {
super.unregister();
HandlerList.unregisterAll(this);
}
@Override
public @NotNull ListenerPriority getPriority() {
return ListenerPriority.LOW;
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.LOW)
public void onBlockInteraction(@NotNull PlayerInteractEvent event) {
Action action = event.getAction();
Player player = event.getPlayer();
Block block = event.getClickedBlock();
if (block == null) return;
if (block == null)
return;
BlockFace face = event.getBlockFace();
// Handle event
boolean allowEvent = true;
if (action == Action.LEFT_CLICK_AIR || action == Action.LEFT_CLICK_BLOCK) {
allowEvent = handle(player, null, null);
} else if (action == Action.RIGHT_CLICK_BLOCK) {
allowEvent = handle(player, block, face);
}
// Cancel event (if needed)
if (!allowEvent) {
event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true, priority = EventPriority.MONITOR)
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
UUID uuid = event.getPlayer().getUniqueId();
SelectBlockTask task = instances.get(uuid);
if (task != null) {
task.cancel();
}
}
@Override
public boolean onAttack(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
return handle(player, null, null);
}
@Override
public boolean onInteract(@NotNull Player player, @NotNull Block block, @NotNull BlockFace face) {
return handle(player, block, face);
}
private boolean handle(@NotNull Player player, @Nullable Block block, @Nullable BlockFace face) {
// Get task responsible for handling this event
UUID uuid = player.getUniqueId();
SelectBlockTask task = instances.get(uuid);
if (task == null) return true;
if (task == null)
return true;
// Cancel task
task.cancel();
// Notify failure listener
if (block == null || face == null) {
if (task.failure != null) {
task.failure.run();
}
return false;
}
// Notify success listener
if (task.success != null) {
task.success.accept(block.getLocation(), face);
}
return false;
}
}
}
+10 -7
View File
@@ -1,20 +1,23 @@
name: ${project.name}
version: ${project.version}
name: Yamipa-OG
version: '${version}'
main: io.josemmo.bukkit.plugin.YamipaPlugin
api-version: 1.16
api-version: '${apiVersion}'
depend: [ProtocolLib]
softdepend:
- BentoBox
- GriefPrevention
- GroupManager
- Hyperverse
- LuckPerms
- Multiverse-Core
- My_Worlds
- PhantomWorlds
- Towny
- WorldGuard
commands:
yamipa:
description: Manage Yamipa images
usage: /yamipa [subcommand]
aliases:
- image
permissions:
# All plugin permissions
yamipa.*: