commit 503dcf5ef943b03cb3dcbae3c6a0cc3994d61b9d Author: Ivy Collective Date: Sun Jan 12 18:28:19 2025 -0500 init diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..dcf1dce --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,58 @@ +name: Build and Publish +on: + push: + branches: + - main +jobs: + build-and-publish: + runs-on: docker + steps: + - name: Checkout Code + uses: actions/checkout@v3 + + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Setup Gradle + uses: actions/gradle/setup-gradle@v4 + with: + gradle-version: '8.11' + + - name: Build with Gradle + run: gradle -Pversion=v${{ github.run_number }} build + + - name: Move JARs + run: mkdir staging && cp build/libs/*.jar staging + + - name: Create Release + uses: "actions/gitea-release-action@v1" + with: + name: Build v${{ github.run_number }} + prerelease: false + tag_name: "v${{ github.run_number }}" + files: |- + staging/blazinggames-v${{ github.run_number }}.jar + + - name: Upload Server Files + uses: "actions/pterodactyl-upload-action@v2.4" + with: + panel-host: ${{ secrets.PANEL_HOST }} + api-key: ${{ secrets.PANEL_API_KEY }} + server-id: ${{ secrets.PANEL_SERVER_ID }} + source: staging/blazinggames-v${{ github.run_number }}.jar + target: "./plugins/blazinggames-latest-ci-cd.jar" + restart: true + + - name: Send Webhook Message + uses: actions/discord-webhook@v6.0.0 + with: + webhook-url: ${{ secrets.WEBHOOK_URL }} + content: "Build v${{ github.run_number }} sucessful" + embed-title: "Build v${{ github.run_number }} sucessful" + embed-description: "${{ github.event.head_commit.message }}" + embed-footer-text: "Pushed by ${{ github.event.pusher.username || 'missing username' }} <${{ github.event.pusher.email || 'missing email' }}>" + embed-color: 5560444 #54d87c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..453250b --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# User-specific stuff +.idea/ + +*.iml +*.ipr +*.iws + +# IntelliJ +out/ +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Cache of project +.gradletasknamecache + +**/build/ + +# Common working directory +run/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# why +bin + +# testing server +/testsrv/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..629f62f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,40 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive", + "java.compile.nullAnalysis.mode": "automatic", + "remote.autoForwardPortsFallback": 0, + "psi-header.config": { + "license": "Apache-2.0", + "author": "The Blazing Games Maintainers", + "copyrightHolder": "The Blazing Games Maintainers", + "forceToTop": true + }, + "psi-header.variables": [ + ["start", "2025"] + ], + "psi-header.templates": [ + { + "language": "*", + "template": [ + "Copyright <> <>", + "", + "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", + "", + " http://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." + ] + } + ], + "psi-header.changes-tracking": { + "autoHeader": "manualSave", + "enforceHeader": true, + "exclude": ["jsonc", "json", "markdown", "plaintext"], + "include": ["java", "html"] + } +} \ No newline at end of file diff --git a/CONFIG.md b/CONFIG.md new file mode 100644 index 0000000..317db18 --- /dev/null +++ b/CONFIG.md @@ -0,0 +1,163 @@ +# jda + +## enabled + +boolean, true if you want discord chat sync to be enabled. all values are ignored if this is false + +## token + +string, your discord bot token + +## link-channel + +int, id of the channel to send event notifications to and to listen for chat messages + +## console-channel + +int, id of the channel to send console logs to and listen for messages with console commands + +## webhook + +string, url of the discord webhook to send chat messages to. should be in the same channel as link-channel + + + +# logging + +## log-error + +boolean, true if you want to log errors + +## log-info + +boolean, true if you want to log regular information to the console + +## log-debug + +boolean, true if you want to log debug info, useful if you're developing the plugin + +## notify-ops-on-error + +boolean, true if you want to send a message to online operators when an error occurs (requires log-error to be true) + + + +# computing + +## local + +### disable-computers + +boolean, true if you want to disable computer management. all other computing settings are ignored if this is true + +### privileges + +#### chunkloading + +boolean, true if you want to allow chunkloading via an upgrade + +#### net + +boolean, true if you want to allow real internet access + +## microsoft + +### spoof-ms-server + +boolean, true if you want to allow anyone to login as any username/uuid without logging in. useful for debugging. + +### client-id + +string, your microsoft app's client id. ignored if spoof-ms-server is true + +### client-secret + +string, your microsoft app'sclient secret. ignored if spoof-ms-server is true + +## jwt + +### secret-key + +string, your jwt secret key. set to randomize-on-server-start if you want to let the server generate one + +### secret-key-is-password + +boolean, true if you want to use the secret key as a password, instead of base64 decoding it + +## services + +### blazing-api and blazing-wss + +#### enabled + +boolean, true if you want to allow this web server to be reached on the internet. all other service-spesific values are ignored if this is false + +#### find-at + +url, where the service can be located on the internet + +#### bind + +##### port + +int, port to bind the service to + +##### https + +###### enabled + +boolean, if the service should use an https server instead of an http one. all other https values are ignored if this is false + +###### password + +string, password for the p12 file + +###### file + +string, path to the p12 cert file relative to the `/plugins/blazinggames` folder + +#### proxy + +##### in-use + +boolean, true if the service is behind a reverse proxy. allows for getting ip addresses from headers and limiting connecting ips. all other proxy values are ignored if this is false + +##### ip-address-header + +boolean, header to get ip addresses from + +##### allow-all + +boolean, true if you want to allow all connecting ips + +##### allowed-ipv4 and allowed-ipv6 + +string[], ip addresses that are allowed to connect to the service. ignored if allow-all is true + + + +# docs + +information to show under the docs at the root of the computing-api. + +all values are ignored if computing.services.blazing-api.enabled is false + +## official-instance + +name, url: information of the official link to the website to manage computers at + +## user-links and developer-links + +links to show on the homepage. + +array of object: name, url + +## notice + +information to show under the notice bar at the bottom of the homepage. + +show: if the noticebar should be shown + +title, description: text to show + +button-title, button-url: action button diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..0121a64 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +Copyright 2025 The Blazing Games Maintainers +This software is free software, licensed under the Apache License (vesion 2.0). For more information, please read the LICENSE file. diff --git a/PROTOCOL.md b/PROTOCOL.md new file mode 100644 index 0000000..385cd63 --- /dev/null +++ b/PROTOCOL.md @@ -0,0 +1,65 @@ +# Websocket Protocol + +JSON format (for both serverbound and clientbound): + +```json +{ + "type": "...", + "payload": {} +} +``` + +You are expected to compress your messages with gzip when sending messages. Recieved messages are also gzipped. + +## Types (common keys) + +If a property is described as a "common key" in a comment, it is a property in the list below. + +**actioner**: the minecraft uuid of the user who did the action. + +**uuid**: the minecraft uuid of the user connected + +**display**: the minecraft username of the user connected + +**x**: index of the character the cursor is on. 0 is before the first char, 1 is after the first char, 2 is after the second char, and so on + +**y**: line the cursor is on. starts at 1 (the first line) + +**timeout**: number of seconds until this client disconnects due to an expried token + +## Disconnect codes + +* 1000: room closed by admin (clientbound), user left room (serverbound) +* 1001: server is shutting down or restarting (clientbound), tab/app/etc closed (serverbound) +* 1009: message is too large (bidirectional) +* 1011: unresolved error (bidirectional) +* 1014: bad gateway (clientbound) +* 3000: bad authorization header, or authorization expired (clientbound) +* 3003: no permissions (clientbound) +* 3008: keepalive timeout (bidirectional) + +## Handshake + +Use the following headers when connecting to the wss: +* `Authorization`: `Bearer ___` where `___` is the JWT given to the app by the blazing api. +* `Blazing-Computer-Id`: set to the id of computer that you want to join the room of + +## Keepalive / heartbeat + +Whenever you recieve a `PING`, you should respond with a `PONG`: this is part of the websocket protocol, not this protocol. + +Your websocket client may already do this for you. If you're unsure, check your client's documentation. + +## User list update: `usrupd` (clientbound) + +**Clientbound**: Sent when a user joins or leaves. The format is as follows: + +```json +{ + "actioner": "84a33aad-bacf-40b4-a1b5-910300e7e3fc", // common key + "type": true // true if join, false if leave +} +``` + + +## tbd diff --git a/README.md b/README.md new file mode 100644 index 0000000..543265a --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# blazing-games-plugin + +The plugin powering the Blazing Games minecraft server, with computers, enchanting altars, spawner modification, and more! + +## Usage + +Releases of prebuilt jars are [available here](https://git.ivycollective.dev/ivycollective/blazing-games-plugin/releases). Otherwise, please build the plugin yourself (see the Development section). + +Instructions: Place the plugin's jar file inside your `plugins` folder and restart your server. + +Most features should be configured out of the box. For those needing advanced configuration, see the `CONFIG.md` file. + +## Development + +This is a standard Paper plugin using Gradle. + +To build, use: `./gradlew build` + +## License + +This plugin is licensed under the Apache License (version 2.0). For more information, please read the NOTICE and LICENSE files. + diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..13a6b52 --- /dev/null +++ b/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'java' + id("io.papermc.paperweight.userdev") version "1.7.1" +} + +group = 'de.blazemcworld' +version = project.version + +repositories { + mavenCentral() + maven { + name = "papermc-repo" + url = "https://repo.papermc.io/repository/maven-public/" + } + maven { + name = "sonatype" + url = "https://oss.sonatype.org/content/groups/public/" + } +} + +dependencies { + // IMPORTANT IF YOU"RE ADDING OR UPDATING DEPENDENCIES!!!!!!!!! + // add them in plugin.yml too so that they are loaded when running on a server + paperweight.paperDevBundle("1.21-R0.1-SNAPSHOT") + compileOnly "io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT" + + // ULID + implementation 'io.azam.ulidj:ulidj:1.0.4' + + // JDA + implementation "net.dv8tion:JDA:5.0.0-beta.23" + implementation "club.minnced:discord-webhooks:0.8.4" + + // Database (HikariCP + popular drivers) + implementation "com.zaxxer:HikariCP:5.1.0" + implementation "org.xerial:sqlite-jdbc:3.45.3.0" + implementation "com.mysql:mysql-connector-j:8.4.0" + implementation "org.postgresql:postgresql:42.7.3" + + // JS Runtime + implementation "com.caoccao.javet:javet:3.1.2" + implementation "com.github.ben-manes.caffeine:caffeine:3.1.8" + + // Web Server + implementation "io.jsonwebtoken:jjwt-api:0.12.6" + implementation "io.jsonwebtoken:jjwt-impl:0.12.6" + implementation "io.jsonwebtoken:jjwt-gson:0.12.6" + implementation "org.freemarker:freemarker:2.3.33" + + // Websocket server + implementation 'org.java-websocket:Java-WebSocket:1.5.7' +} + +def targetJavaVersion = 21 +java { + def javaVersion = JavaVersion.toVersion(targetJavaVersion) + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + if (JavaVersion.current() < javaVersion) { + toolchain.languageVersion = JavaLanguageVersion.of(targetJavaVersion) + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' + + if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { + options.release.set(targetJavaVersion) + } +} + +processResources { + def props = [version: version] + inputs.properties props + filteringCharset 'UTF-8' + filesMatching('plugin.yml') { + expand props + } +} + +tasks.assemble { + dependsOn(reobfJar) +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..838736c --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version = STAGING \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..b82aa23 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..1aa94a4 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/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. +# + +############################################################################## +# +# 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/subprojects/plugins/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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# 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, 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" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# 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" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@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 + +@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=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +: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 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..45af48e --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { + url 'https://repo.papermc.io/repository/maven-public/' + } + } +} + +rootProject.name = 'blazinggames' diff --git a/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java b/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java new file mode 100644 index 0000000..ada0aa4 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/BlazingGames.java @@ -0,0 +1,295 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import de.blazemcworld.blazinggames.commands.*; +import de.blazemcworld.blazinggames.computing.ComputerRegistry; +import de.blazemcworld.blazinggames.computing.ComputerRegistry.ComputerPrivileges; +import de.blazemcworld.blazinggames.computing.api.ComputingAPI; +import de.blazemcworld.blazinggames.utils.Cooldown; +import de.blazemcworld.blazinggames.utils.ItemStackTypeAdapter; +import de.blazemcworld.blazinggames.utils.TextLocation; +import de.blazemcworld.blazinggames.discord.*; +import de.blazemcworld.blazinggames.events.*; +import de.blazemcworld.blazinggames.items.CustomRecipes; +import de.blazemcworld.blazinggames.teleportanchor.LodestoneInteractionEventListener; +import de.blazemcworld.blazinggames.teleportanchor.LodestoneInventoryClickEventListener; +import io.jsonwebtoken.Jwts.SIG; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.io.DecodingException; +import io.jsonwebtoken.io.Encoders; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.WeakKeyException; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.Server; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.PluginCommand; +import org.bukkit.command.TabCompleter; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.inventory.ItemStack; +import org.bukkit.plugin.PluginManager; +import org.bukkit.plugin.java.JavaPlugin; + +import java.lang.reflect.Modifier; +import java.util.Objects; + +import javax.crypto.SecretKey; + +@SuppressWarnings("unused") +public final class BlazingGames extends JavaPlugin { + public boolean API_AVAILABLE = false; + + // Gson + public static final Gson gson = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.PRIVATE, Modifier.PROTECTED, Modifier.TRANSIENT, Modifier.STATIC) .registerTypeAdapter(Location.class, new TextLocation.LocationTypeAdapter()) + .registerTypeAdapter(ItemStack.class, new ItemStackTypeAdapter()) + .registerTypeAdapter(Location.class, new TextLocation.LocationTypeAdapter()) + .create(); + + // Cooldowns + public Cooldown interactCooldown; + + // Logging + private boolean logErrors = true; + private boolean logInfo = true; + private boolean logDebug = false; + private boolean notifyOpsOnError = true; + + // Computers + private boolean computersEnabled = false; + private ComputerPrivileges computerPrivileges = ComputerPrivileges.minimal(); + + @Override + public void onEnable() { + // Config + saveDefaultConfig(); + FileConfiguration config = getConfig(); + + // Log levels + logErrors = config.getBoolean("logging.log-error"); + logInfo = config.getBoolean("logging.log-info"); + logDebug = config.getBoolean("logging.log-debug"); + notifyOpsOnError = config.getBoolean("logging.notify-ops-on-error"); + + // Computers + computersEnabled = !config.getBoolean("computing-local.disable-computers"); + if (computersEnabled) { + computerPrivileges = new ComputerPrivileges( + config.getBoolean("computing-local.privileges.chunkloading"), + config.getBoolean("computing-local.privileges.net") + ); + } + + // Discord + if (config.getBoolean("jda.enabled")) { + AppConfig appConfig = new AppConfig( + config.getString("jda.token"), + config.getLong("jda.link-channel"), + config.getLong("jda.console-channel"), + config.getString("jda.webhook") + ); + + try { + DiscordApp.init(appConfig); + DiscordApp.send(DiscordNotification.serverStartup()); + } catch (IllegalArgumentException e) { + getLogger().severe("Failed to start JDA"); + BlazingGames.get().log(e); + } + } + + // Recipes + ComputerRegistry.registerAllRecipes(); + if (computersEnabled) CustomRecipes.loadRecipes(); + + // Computers + if (!config.getBoolean("computing.local.disable-computers") && ( + config.getBoolean("computing.services.blazing-api.enabled") || + config.getBoolean("computing.services.blazing-wss.enabled") + )) { + log("API or WSS enabled, starting..."); + + // Load JWT + String existingKey = config.getString("computing.jwt.secret-key"); + boolean isPassword = config.getBoolean("computing.jwt.secret-key-is-password"); + SecretKey key; + if (existingKey == null || existingKey.equals("randomize-on-server-start")) { + SecretKey newKey = SIG.HS256.key().build(); + config.set("computing.jwt.secret-key", Encoders.BASE64.encode(newKey.getEncoded())); + config.set("computing.jwt.secret-key-is-password", false); + key = newKey; + saveConfig(); + } else if (isPassword) { + key = Keys.password(existingKey.toCharArray()); + } else { + try { + key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(existingKey)); + } catch (DecodingException | WeakKeyException e) { + log(e); + key = null; + } + } + + // Microsoft + boolean spoofMsServer = config.getBoolean("computing.microsoft.spoof-ms-server"); + String clientId = config.getString("computing.microsoft.client-id"); + String clientSecret = config.getString("computing.microsoft.client-secret"); + + if (key != null) { + var apiConfig = ComputingAPI.WebsiteConfig.auto(config, "computing.services.blazing-api"); + var wssConfig = ComputingAPI.WebsiteConfig.auto(config, "computing.services.blazing-wss"); + + ComputingAPI.setConfig(new ComputingAPI.Config(spoofMsServer, clientId, clientSecret, key, apiConfig, wssConfig)); + API_AVAILABLE = ComputingAPI.startAll(); + + if (API_AVAILABLE) { + log("API and/or WSS started"); + } else { + getLogger().severe("Failed to start API and/or WSS (see above)"); + } + } else { + getLogger().severe("Failed to start API and/or WSS (missing key)"); + API_AVAILABLE = false; + } + } else { + API_AVAILABLE = false; + } + + // Commands + registerCommand("customenchant", new CustomEnchantCommand()); + registerCommand("customgive", new CustomGiveCommand()); + registerCommand("killme", new KillMeCommand()); + registerCommand("playtime", new PlaytimeCommand()); + registerCommand("config", new ConfigCommand()); + registerCommand("setaltar", new SetAltar()); + + // Events + PluginManager pluginManager = getServer().getPluginManager(); + pluginManager.registerEvents(new PrepareAnvilEventListener(), this); + pluginManager.registerEvents(new PrepareGrindstoneEventListener(), this); + pluginManager.registerEvents(new ClickInventorySlotEventListener(), this); + pluginManager.registerEvents(new ChatEventListener(), this); + pluginManager.registerEvents(new ClickEntityEventListener(), this); + pluginManager.registerEvents(new BreakBlockEventListener(), this); + pluginManager.registerEvents(new EntityDeathEventListener(), this); + pluginManager.registerEvents(new AdvancementEventListener(), this); + pluginManager.registerEvents(new JoinEventListener(), this); + pluginManager.registerEvents(new QuitEventListener(), this); + pluginManager.registerEvents(new InteractEventListener(), this); + pluginManager.registerEvents(new EntityDamagedByEventListener(), this); + pluginManager.registerEvents(new BlockPlaceEventListener(), this); + pluginManager.registerEvents(new SpawnerSpawnEventListener(), this); + pluginManager.registerEvents(new LodestoneInteractionEventListener(), this); + pluginManager.registerEvents(new LodestoneInventoryClickEventListener(), this); + pluginManager.registerEvents(new BlockDestroyEventListener(), this); + pluginManager.registerEvents(new BlockExplodeEventListener(), this); + pluginManager.registerEvents(new EntityExplodeEventListener(), this); + pluginManager.registerEvents(new DeathEventListener(), this); + pluginManager.registerEvents(new LootGenerateEventListener(), this); + pluginManager.registerEvents(new PiglinBarterEventListener(), this); + pluginManager.registerEvents(new VillagerAcquireTradeEventListener(), this); + pluginManager.registerEvents(new InventoryDragEventListener(), this); + pluginManager.registerEvents(new InventoryCloseEventListener(), this); + + Bukkit.getScheduler().runTaskTimer(this, TickEventListener::onTick, 0, 1); + + // Cooldowns + interactCooldown = new Cooldown(this); + } + + @Override + public void onDisable() { + // Discord + DiscordApp.send(DiscordNotification.serverShutdown()); + DiscordApp.dispose(); + + // Recipes + CustomRecipes.unloadRecipes(); + + // Computers + ComputingAPI.stopAll(); + API_AVAILABLE = false; // reset value + } + + private void registerCommand(String name, CommandExecutor executor) { + PluginCommand command = Objects.requireNonNull(getCommand(name)); + command.setExecutor(executor); + + if(executor instanceof TabCompleter tc) { + command.setTabCompleter(tc); + } + } + + public static BlazingGames get() + { + return (BlazingGames) BlazingGames.getProvidingPlugin(BlazingGames.class); + } + + public void log(Object o) { + if (o instanceof Exception exception) { + if (logErrors) { + getLogger().severe(""); + getLogger().severe("An exception occurred:"); + getLogger().severe(exception.getClass().getName()); + getLogger().severe(exception.getMessage()); + StackTraceElement[] stackTrace = exception.getStackTrace(); + for (StackTraceElement element : stackTrace) { + getLogger().severe(element.toString()); + } + getLogger().severe(""); + if (notifyOpsOnError) { + Bukkit.broadcast(Component.text( + "An exception occurred: " + exception.getMessage() + " (" + exception.getClass().getName() + + ") - " + "For more info, see the console." + ).color(NamedTextColor.RED), Server.BROADCAST_CHANNEL_ADMINISTRATIVE); + } + } + } else { + if (logInfo) { + String s = String.valueOf(o); + getLogger().info(s); + } + } + } + + public void debugLog(Object o) { + if (logDebug) { + log(o); + } + } + + public NamespacedKey key(String id) { + return new NamespacedKey(this, id); + } + + public boolean areComputersEnabled() { + return computersEnabled; + } + + public ComputerPrivileges getComputerPrivileges() { + return computerPrivileges; + } + + public boolean isApiAvailable() { + return API_AVAILABLE; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderLocation.java b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderLocation.java new file mode 100644 index 0000000..b35aa8a --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderLocation.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.builderwand; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; + +public class BuilderLocation { + protected final Location location; + + public BuilderLocation(Location location) { + this.location = location.clone(); + } + + public BuilderLocation(BuilderLocation other) { + this.location = other.location.clone(); + } + + // returns a COPY of the BuilderLocation offset by a block face + public BuilderLocation add(BlockFace face) { + return add(face.getDirection()); + } + + // returns a COPY of the BuilderLocation offset by a vector + public BuilderLocation add(Vector vector) { + return add(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); + } + + // returns a COPY of the BuilderLocation offset by a trio of integers + public BuilderLocation add(int modX, int modY, int modZ) { + BuilderLocation result = new BuilderLocation(this); + + result.location.add(modX, modY, modZ); + + return result; + } + + public boolean canPlaceOnLocation(Player player, Material stack) { + return location.getBlock().getType() == stack; + } + + public boolean placeBlock(Player player, Material stack) { + if(stack.isBlock() && canPlaceAtLocation(player, stack)) { + location.getBlock().setBlockData(stack.createBlockData()); + return true; + } + return false; + } + + public boolean canPlaceAtLocation(Player player, Material stack) { + Block block = this.location.getBlock(); + + BlockData data = stack.createBlockData(); + if(location.getBlock().canPlace(data)) { + return block.isEmpty() || block.isReplaceable(); + } + + return false; + } + + @Override + public String toString() { + return location.toString(); + } + + @Override + public boolean equals(Object other) { + if(other instanceof BuilderLocation loc) { + return location.equals(loc.location); + } + + return false; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderSlabLocation.java b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderSlabLocation.java new file mode 100644 index 0000000..ffdd6d5 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderSlabLocation.java @@ -0,0 +1,175 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.builderwand; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Slab; +import org.bukkit.entity.Player; + +public class BuilderSlabLocation extends BuilderLocation { + public enum BuilderSlabHalf { + TOP, + BOTTOM + } + + BuilderSlabHalf half; + + public BuilderSlabLocation(Location location, BuilderSlabHalf half) { + super(location); + this.half = half; + } + + public BuilderSlabLocation(BuilderSlabLocation other) { + super(other); + this.half = other.half; + } + + // returns a COPY of the BuilderLocation offset by a trio of integers + public BuilderSlabLocation add(int modX, int modY, int modZ) { + BuilderSlabLocation result = new BuilderSlabLocation(this); + + int yMov = 0; + + if(modY != 0) { + boolean inverted = modY < 0; + if(inverted) modY = -modY; + + for(int i = 0; i < modY; i++) { + if(result.half == BuilderSlabHalf.BOTTOM) { + if(inverted) { + yMov--; + } + result.half = BuilderSlabHalf.TOP; + } + else { + if(!inverted) { + yMov++; + } + result.half = BuilderSlabHalf.BOTTOM; + } + } + } + + result.location.add(modX, yMov, modZ); + + return result; + } + + public boolean canPlaceOnLocation(Player player, Material stack) { + Block block = this.location.getBlock(); + + if(block.getType() != stack) { + return false; + } + + if(block.getBlockData() instanceof Slab slab) { + if(slab.getPlacementMaterial() != stack) { + return false; + } + + if(half == BuilderSlabHalf.TOP) { + return slab.getType() != Slab.Type.BOTTOM; + } + else { + return slab.getType() != Slab.Type.TOP; + } + } + + return true; + } + + public boolean canPlaceAtLocation(Player player, Material stack) { + Block block = this.location.getBlock(); + + if(block.getBlockData() instanceof Slab slab) { + if(slab.getPlacementMaterial() != stack) { + return false; + } + + if(half == BuilderSlabHalf.TOP) { + return slab.getType() == Slab.Type.BOTTOM; + } + else { + return slab.getType() == Slab.Type.TOP; + } + } + + return block.isEmpty() || block.isReplaceable(); + } + + public boolean placeBlock(Player player, Material stack) { + if(stack.isBlock() && canPlaceAtLocation(player, stack)) { + if(location.getBlock().getBlockData() instanceof Slab slab) { + if(location.getBlock().getType() != stack) { + return false; + } + + if(slab.getType() == Slab.Type.TOP) { + if(half == BuilderSlabHalf.BOTTOM) { + slab.setType(Slab.Type.DOUBLE); + location.getBlock().setBlockData(slab); + return true; + } + } + else if(slab.getType() == Slab.Type.BOTTOM) { + if(half == BuilderSlabHalf.TOP) { + slab.setType(Slab.Type.DOUBLE); + location.getBlock().setBlockData(slab); + return true; + } + } + return false; + } + + BlockData data = stack.createBlockData(); + + if(data instanceof Slab slab) { + if(half == BuilderSlabHalf.TOP) { + slab.setType(Slab.Type.TOP); + } + else if(half == BuilderSlabHalf.BOTTOM) { + slab.setType(Slab.Type.BOTTOM); + } + } + + location.getBlock().setBlockData(data); + + return true; + } + return false; + } + + @Override + public String toString() { + return super.toString() + ":" + half.name(); + } + + @Override + public boolean equals(Object other) { + if(!super.equals(other)) { + return false; + } + + if(other instanceof BuilderSlabLocation loc) { + return half == loc.half; + } + + return false; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderWand.java b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderWand.java new file mode 100644 index 0000000..a2850a0 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderWand.java @@ -0,0 +1,252 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.builderwand; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.items.CustomItem; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.GameMode; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.type.Slab; +import org.bukkit.entity.Player; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class BuilderWand extends CustomItem { + private static final NamespacedKey modeKey = BlazingGames.get().key("builder_mode"); + + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("builder_wand"); + } + + @Override + protected @NotNull ItemStack material() { + ItemStack wand = new ItemStack(Material.BLAZE_ROD); + + ItemMeta meta = wand.getItemMeta(); + + meta.itemName(Component.text("Builder's Wand").color(NamedTextColor.GOLD)); + meta.setEnchantmentGlintOverride(true); + + meta.getPersistentDataContainer().set(modeKey, BuilderWandMode.persistentType, BuilderWandMode.NO_LOCK); + + wand.setItemMeta(meta); + + return wand; + } + + @Override + protected @NotNull ItemStack modifyMaterial(ItemStack wand) { + return updateWand(wand); + } + + private ItemStack updateWand(ItemStack wand) { + if(!matchItem(wand)) { + return wand; + } + + ItemStack result = wand.clone(); + + ItemMeta meta = result.getItemMeta(); + + meta.lore(List.of(Component.text(getModeText(wand)).color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, false))); + + result.setItemMeta(meta); + + return result; + } + + public ItemStack cycleMode(ItemStack wand) { + if(!matchItem(wand)) { + return wand; + } + + ItemStack result = wand.clone(); + + ItemMeta meta = wand.getItemMeta(); + + BuilderWandMode mode = meta.getPersistentDataContainer().getOrDefault(modeKey, BuilderWandMode.persistentType, BuilderWandMode.NO_LOCK); + mode = mode.getNextMode(); + + meta.getPersistentDataContainer().set(modeKey, BuilderWandMode.persistentType, mode); + + result.setItemMeta(meta); + + return updateWand(result); + } + + public String getModeText(ItemStack wand) { + if(!matchItem(wand)) { + return ""; + } + + ItemMeta meta = wand.getItemMeta(); + + BuilderWandMode mode = meta.getPersistentDataContainer().getOrDefault(modeKey, BuilderWandMode.persistentType, BuilderWandMode.NO_LOCK); + + return "Mode: " + mode.getModeText(); + } + + public int build(Player player, ItemStack eventItem, Block block, BlockFace face, Vector interactionPoint) { + if(!matchItem(eventItem)) { + return 0; + } + + BuilderWandMode mode = eventItem.getItemMeta().getPersistentDataContainer().getOrDefault(modeKey, BuilderWandMode.persistentType, BuilderWandMode.NO_LOCK); + + if(!mode.canBuildOnFace(face)) { + return 0; + } + + Material placementMaterial = block.getBlockData().getPlacementMaterial(); + Material itemMaterial = block.getBlockData().getMaterial(); + + int maxBlocks = 0; + + Inventory inv = player.getInventory(); + + for (ItemStack itemStack : inv.getStorageContents()) { + if(itemStack == null) continue; + if(itemStack.getType() != itemMaterial) continue; + if(CustomItem.isCustomItem(itemStack)) continue; + + maxBlocks += itemStack.getAmount(); + } + + if(maxBlocks > 128 || player.getGameMode() == GameMode.CREATIVE) { + maxBlocks = 128; + } + + BuilderLocation location = getBuilderLocationFromInteractionPoint(block, interactionPoint); + + List locations = getBuilderWandLocations(player, face, location, mode, placementMaterial, maxBlocks); + + int blocksUsed = 0; + + for(BuilderLocation currentLocation : locations) { + if(currentLocation.add(face).placeBlock(player, placementMaterial)) { + blocksUsed++; + } + } + + if(blocksUsed <= 0) { + return 0; + } + + int copy = blocksUsed; + + if(player.getGameMode() != GameMode.CREATIVE) { + for (ItemStack itemStack : inv.getStorageContents()) { + if(blocksUsed <= 0) break; + if(itemStack == null) continue; + if(itemStack.getType() != itemMaterial) continue; + if(CustomItem.isCustomItem(itemStack)) continue; + + if(itemStack.getAmount() <= blocksUsed) { + blocksUsed -= itemStack.getAmount(); + itemStack.subtract(itemStack.getAmount()); + } + else { + int a = blocksUsed; + blocksUsed -= a; + itemStack.subtract(a); + } + } + } + + return copy; + } + + private static List getBuilderWandLocations(Player player, BlockFace face, BuilderLocation location, BuilderWandMode mode, Material placementMaterial, int maxBlocks) { + List locations = new ArrayList<>(); + locations.add(location); + + List validFaces = new ArrayList<>(mode.getBuildDirections()); + validFaces.remove(face); + validFaces.remove(face.getOppositeFace()); + + for(int i = 0; i < locations.size(); i++) { + BuilderLocation currentLocation = locations.get(i); + + for(BlockFace spreadFace : validFaces) { + BuilderLocation spreadLocation = currentLocation.add(spreadFace); + BuilderLocation buildLocation = spreadLocation.add(face); + + if(!locations.contains(spreadLocation)) { + if(spreadLocation.canPlaceOnLocation(player, placementMaterial)) { + if(buildLocation.canPlaceAtLocation(player, placementMaterial)) { + locations.add(spreadLocation); + } + } + } + + if(locations.size() >= maxBlocks) { + break; + } + } + + if(locations.size() >= maxBlocks) { + break; + } + } + + return locations; + } + + private static @NotNull BuilderLocation getBuilderLocationFromInteractionPoint(Block block, Vector interactionPoint) { + BuilderLocation location = new BuilderLocation(block.getLocation()); + + if(block.getBlockData() instanceof Slab slab) { + BuilderSlabLocation.BuilderSlabHalf half = switch(slab.getType()) { + case TOP -> BuilderSlabLocation.BuilderSlabHalf.TOP; + case BOTTOM -> BuilderSlabLocation.BuilderSlabHalf.BOTTOM; + case DOUBLE -> interactionPoint.getY() > 0.5 ? BuilderSlabLocation.BuilderSlabHalf.TOP + : BuilderSlabLocation.BuilderSlabHalf.BOTTOM; + }; + + location = new BuilderSlabLocation(block.getLocation(), half); + } + return location; + } + + public Map getRecipes() { + ShapedRecipe wandRecipe = new ShapedRecipe(getKey(), create()); + wandRecipe.shape( + " S", + " R ", + "R " + ); + wandRecipe.setIngredient('R', Material.BLAZE_ROD); + wandRecipe.setIngredient('S', Material.NETHER_STAR); + + return Map.of( + getKey(), wandRecipe + ); + } +} + diff --git a/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderWandMode.java b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderWandMode.java new file mode 100644 index 0000000..a4cd4dc --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/builderwand/BuilderWandMode.java @@ -0,0 +1,139 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.builderwand; + +import com.google.common.collect.ImmutableList; +import de.blazemcworld.blazinggames.utils.EnumDataType; +import org.bukkit.block.BlockFace; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public enum BuilderWandMode { + NO_LOCK("No Lock", (m) -> { + m.allowedFaces.add(BlockFace.NORTH); + m.allowedFaces.add(BlockFace.SOUTH); + m.allowedFaces.add(BlockFace.EAST); + m.allowedFaces.add(BlockFace.WEST); + m.allowedFaces.add(BlockFace.UP); + m.allowedFaces.add(BlockFace.DOWN); + + m.buildDirections.add(BlockFace.NORTH); + m.buildDirections.add(BlockFace.SOUTH); + m.buildDirections.add(BlockFace.EAST); + m.buildDirections.add(BlockFace.WEST); + m.buildDirections.add(BlockFace.UP); + m.buildDirections.add(BlockFace.DOWN); + }), + HORIZONTAL("Horizontal", (m) -> { + m.allowedFaces.add(BlockFace.NORTH); + m.allowedFaces.add(BlockFace.SOUTH); + m.allowedFaces.add(BlockFace.EAST); + m.allowedFaces.add(BlockFace.WEST); + + m.buildDirections.add(BlockFace.NORTH); + m.buildDirections.add(BlockFace.SOUTH); + m.buildDirections.add(BlockFace.EAST); + m.buildDirections.add(BlockFace.WEST); + }), + VERTICAL("Vertical", (m) -> { + m.allowedFaces.add(BlockFace.NORTH); + m.allowedFaces.add(BlockFace.SOUTH); + m.allowedFaces.add(BlockFace.EAST); + m.allowedFaces.add(BlockFace.WEST); + + m.buildDirections.add(BlockFace.UP); + m.buildDirections.add(BlockFace.DOWN); + }), + NORTH_SOUTH("North-South", (m) -> { + m.allowedFaces.add(BlockFace.EAST); + m.allowedFaces.add(BlockFace.WEST); + m.allowedFaces.add(BlockFace.UP); + m.allowedFaces.add(BlockFace.DOWN); + + m.buildDirections.add(BlockFace.NORTH); + m.buildDirections.add(BlockFace.SOUTH); + }), + NORTH_SOUTH_VERTICAL("North-South (+ Vertical)", (m) -> { + m.allowedFaces.add(BlockFace.EAST); + m.allowedFaces.add(BlockFace.WEST); + m.allowedFaces.add(BlockFace.UP); + m.allowedFaces.add(BlockFace.DOWN); + + m.buildDirections.add(BlockFace.NORTH); + m.buildDirections.add(BlockFace.SOUTH); + m.buildDirections.add(BlockFace.UP); + m.buildDirections.add(BlockFace.DOWN); + }), + EAST_WEST("East-West", (m) -> { + m.allowedFaces.add(BlockFace.NORTH); + m.allowedFaces.add(BlockFace.SOUTH); + m.allowedFaces.add(BlockFace.UP); + m.allowedFaces.add(BlockFace.DOWN); + + m.buildDirections.add(BlockFace.EAST); + m.buildDirections.add(BlockFace.WEST); + }), + EAST_WEST_VERTICAL("East-West (+ Vertical)", (m) -> { + m.allowedFaces.add(BlockFace.NORTH); + m.allowedFaces.add(BlockFace.SOUTH); + m.allowedFaces.add(BlockFace.UP); + m.allowedFaces.add(BlockFace.DOWN); + + m.buildDirections.add(BlockFace.EAST); + m.buildDirections.add(BlockFace.WEST); + m.buildDirections.add(BlockFace.UP); + m.buildDirections.add(BlockFace.DOWN); + }); + + public static final EnumDataType persistentType = new EnumDataType<>(BuilderWandMode.class); + + private final String modeText; + private final List allowedFaces = new ArrayList<>(); + private final List buildDirections = new ArrayList<>(); + + BuilderWandMode(String modeText, Consumer builder) { + this.modeText = modeText; + builder.accept(this); + } + + public boolean canBuildOnFace(BlockFace face) { + return allowedFaces.contains(face); + } + + public List getBuildDirections() { + ImmutableList.Builder faces = new ImmutableList.Builder<>(); + faces.addAll(buildDirections); + return faces.build(); + } + + public String getModeText() { + return modeText; + } + + public BuilderWandMode getNextMode() { + return switch(this) { + case NO_LOCK -> HORIZONTAL; + case HORIZONTAL -> VERTICAL; + case VERTICAL -> NORTH_SOUTH; + case NORTH_SOUTH -> NORTH_SOUTH_VERTICAL; + case NORTH_SOUTH_VERTICAL -> EAST_WEST; + case EAST_WEST -> EAST_WEST_VERTICAL; + case EAST_WEST_VERTICAL -> NO_LOCK; + }; + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/CommandHelper.java b/src/main/java/de/blazemcworld/blazinggames/commands/CommandHelper.java new file mode 100644 index 0000000..8dc268b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/CommandHelper.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; + +public class CommandHelper { + public static void sendUsage(CommandSender sender, Command cmd) { + sender.sendMessage(Component.text("Usage: " + cmd.getUsage()).color(NamedTextColor.RED)); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/ConfigCommand.java b/src/main/java/de/blazemcworld/blazinggames/commands/ConfigCommand.java new file mode 100644 index 0000000..5b0a792 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/ConfigCommand.java @@ -0,0 +1,121 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import de.blazemcworld.blazinggames.utils.PlayerConfig; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; + +public class ConfigCommand implements CommandExecutor, TabCompleter { + String[] values = {"display", "pronouns", "color"}; + + @Override + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player p)) { + sender.sendMessage(Component.text("Only players can use this command!") + .color(NamedTextColor.RED)); + return false; + } + + if (args.length < 1) { + return false; + } + + String param = args[0]; + String value = (args.length > 1) ? Arrays.stream(args).skip(1).collect(Collectors.joining(" ")) + : null; + + PlayerConfig config = PlayerConfig.forPlayer(p.getUniqueId()); + switch (param) { + case "display": + if (!enforceParam(sender, param, value, 1, 36)) return false; + config.setDisplayName(value); + break; + case "pronouns": + if (!enforceParam(sender, param, value, 1, 16)) return false; + config.setPronouns(value); + break; + case "color": + if (!enforceParam(sender, param, value, 6, 6)) return false; + if (value == null || value.isBlank()) { + config.setNameColor(null); + } else { + int realValue; + try { + realValue = Integer.parseInt(value, 16); + } catch (NumberFormatException e) { + sender.sendMessage(Component.text("Invalid color: #" + value).color(NamedTextColor.RED)); + return false; + } + config.setNameColor(TextColor.color(realValue)); + } + break; + default: + sender.sendMessage(Component.text("Unknown parameter: " + param).color(NamedTextColor.RED)); + return false; + } + sendSuccess(sender, param, value); + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, + @NotNull String label, @NotNull String[] args) { + if (args.length == 1) { + return Arrays.asList(values); + } + return List.of(); + } + + private static void sendSuccess(CommandSender sender, String param, String value) { + if (value == null || value.isBlank()) { + sender.sendMessage(Component.text("Cleared " + param).color(NamedTextColor.GREEN)); + } else { + sender.sendMessage(Component.text("Set " + param + " to " + value).color(NamedTextColor.GREEN)); + } + } + + private static boolean enforceParam(CommandSender sender, String param, String value, int minChars, int maxChars) { + if (value == null || value.isBlank()) { + // return for unset + return true; + } + + if (value.length() < minChars || value.length() > maxChars) { + if (minChars == maxChars) { + sender.sendMessage(Component.text("Parameter " + param + " must be exactly " + minChars + " chars long!").color(NamedTextColor.RED)); + } else { + sender.sendMessage(Component.text("Parameter " + param + " must be between " + minChars + " and " + maxChars + " chars long!").color(NamedTextColor.RED)); + } + return false; + } + return true; + + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/CustomEnchantCommand.java b/src/main/java/de/blazemcworld/blazinggames/commands/CustomEnchantCommand.java new file mode 100644 index 0000000..31ac01b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/CustomEnchantCommand.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class CustomEnchantCommand implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + if (!(commandSender instanceof Player p)) { + commandSender.sendMessage(Component.text("Only players can use this command!") + .color(NamedTextColor.RED)); + return true; + } + + if(strings.length < 1) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + if(strings.length > 2) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + CustomEnchantment enchantment = CustomEnchantments.getByKey(BlazingGames.get().key(strings[0])); + int level = 1; + + if(enchantment == null) + { + commandSender.sendMessage(Component.text("Unknown custom enchantment: " + strings[0] + "!")); + return true; + } + + if(strings.length > 1) { + level = Integer.parseInt(strings[1]); + } + + ItemStack tool = EnchantmentHelper.setCustomEnchantment(p.getInventory().getItemInMainHand(), enchantment, level); + p.getInventory().setItemInMainHand(tool); + + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + List tabs = new ArrayList<>(); + + if(strings.length == 1) { + CustomEnchantments.list().forEach(enchantment -> tabs.add(enchantment.getKey().getKey())); + } + + return tabs; + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/CustomGiveCommand.java b/src/main/java/de/blazemcworld/blazinggames/commands/CustomGiveCommand.java new file mode 100644 index 0000000..df0c2ee --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/CustomGiveCommand.java @@ -0,0 +1,88 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.items.CustomItems; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; + +public class CustomGiveCommand implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + if (!(commandSender instanceof Player p)) { + commandSender.sendMessage(Component.text("Only players can use this command!") + .color(NamedTextColor.RED)); + return true; + } + + if(strings.length < 1) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + if(strings.length > 2) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + CustomItem itemType = CustomItems.getByKey(BlazingGames.get().key(strings[0])); + int count = 1; + + if(itemType == null) + { + commandSender.sendMessage(Component.text("Unknown custom item: " + strings[0] + "!").color(NamedTextColor.RED)); + return true; + } + + if(strings.length > 1) { + count = Integer.parseInt(strings[1]); + } + + ItemStack item = itemType.create(); + item.setAmount(count); + + p.getInventory().addItem(item); + + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + List tabs = new ArrayList<>(); + + if(strings.length == 1) { + CustomItems.list().forEach(itemType -> tabs.add(itemType.getKey().getKey())); + } + + return tabs; + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/KillMeCommand.java b/src/main/java/de/blazemcworld/blazinggames/commands/KillMeCommand.java new file mode 100644 index 0000000..d0fedeb --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/KillMeCommand.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.NotNull; + +public class KillMeCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + if (!(commandSender instanceof Player p)) { + commandSender.sendMessage(Component.text("Only players can use this command!") + .color(NamedTextColor.RED)); + return true; + } + + if(strings.length > 0) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + BukkitRunnable run = new BukkitRunnable() { + int damage = 1; + int damageTick = 0; + + @Override + public void run() { + p.damage(damage); + damageTick++; + if(damageTick >= 10) { + damageTick = 0; + damage++; + } + if(!p.isValid() || p.isDead()) { + cancel(); + } + } + }; + + run.runTaskTimer(BlazingGames.get(), 0, 10); + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/PlaytimeCommand.java b/src/main/java/de/blazemcworld/blazinggames/commands/PlaytimeCommand.java new file mode 100644 index 0000000..b954720 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/PlaytimeCommand.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class PlaytimeCommand implements CommandExecutor { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + if(strings.length > 1) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + OfflinePlayer player; + if(strings.length > 0) { + player = Bukkit.getOfflinePlayer(strings[0]); + } + else { + if(commandSender instanceof Player p) { + player = p; + } + else { + commandSender.sendMessage(Component.text("Only players can use this command without providing a player!") + .color(NamedTextColor.RED)); + return true; + } + } + + int playtime = player.getStatistic(Statistic.PLAY_ONE_MINUTE); + + if(playtime <= 0) { + commandSender.sendMessage(Component.text(player.getName() + " has not been on this server ever before!") + .color(NamedTextColor.RED)); + return true; + } + + int seconds = playtime / 20 % 60; + int minutes = playtime / 20 / 60 % 60; + int hours = playtime / 20 / 60 / 60 % 24; + int days = playtime / 20 / 60 / 60 / 24; + + List time = new ArrayList<>(); + + if(days > 0) time.add(days+" days"); + if(hours > 0) time.add(hours+" hours"); + if(minutes > 0) time.add(minutes+" minutes"); + if(seconds > 0) time.add(seconds+" seconds"); + + StringBuilder timeText = new StringBuilder(); + + timeText.append(time.get(0)); + + if(time.size() > 1) { + for(int i = 1; i < time.size()-1; i++) { + timeText.append(", ").append(time.get(i)); + } + timeText.append(" and ").append(time.get(time.size()-1)); + } + + commandSender.sendMessage(Component.text(player.getName() + " has been wasting time on this server for " + timeText + "!") + .color(NamedTextColor.YELLOW)); + + return true; + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/commands/SetAltar.java b/src/main/java/de/blazemcworld/blazinggames/commands/SetAltar.java new file mode 100644 index 0000000..682f5e2 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/commands/SetAltar.java @@ -0,0 +1,104 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.commands; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.multiblocks.*; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.type.Stairs; +import org.bukkit.command.Command; +import org.bukkit.command.CommandExecutor; +import org.bukkit.command.CommandSender; +import org.bukkit.command.TabCompleter; +import org.bukkit.entity.Player; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; + +public class SetAltar implements CommandExecutor, TabCompleter { + + @Override + public boolean onCommand(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + if (!(commandSender instanceof Player p)) { + commandSender.sendMessage(Component.text("Only players can use this command!") + .color(NamedTextColor.RED)); + return true; + } + + if(strings.length != 1) { + CommandHelper.sendUsage(commandSender, command); + return true; + } + + int level; + try { + level = Integer.parseInt(strings[0]); + } catch (NumberFormatException e) { + commandSender.sendMessage(Component.text("Unknown structure level: " + strings[0] + "!").color(NamedTextColor.RED)); + return true; + } + + MultiBlockStructureMetadata structureMetadata = MultiBlockStructures.getByKey(BlazingGames.get().key("altar_of_enchanting")); + + Location oldLoc = p.getLocation(); + assert structureMetadata != null; + int index = 0; + for (MultiBlockStructure structure : ((MultiLevelBlockStructure) structureMetadata.getStructure()).getLevels()) { + if (index >= level) break; + index += 1; + for (Map.Entry entry : structure.getPredicates().entrySet()) { + Vector vector = entry.getKey(); + BlockPredicate predicate = entry.getValue(); + Location loc = oldLoc.clone().add(vector); + if (predicate instanceof SingleBlockPredicate) { + loc.getBlock().setType(((SingleBlockPredicate) predicate).getMaterial()); + } else if (predicate instanceof ComplexBlockPredicate complexBlockPredicate) { + StairShapeBlockPredicate shape = (StairShapeBlockPredicate) complexBlockPredicate.getPredicates().get(1); + + loc.getBlock().setType(Material.QUARTZ_STAIRS); + Stairs data = (Stairs) loc.getBlock().getBlockData(); + data.setShape((shape.name().startsWith("STRAIGHT") ? Stairs.Shape.STRAIGHT : Stairs.Shape.OUTER_LEFT)); + data.setHalf(Stairs.Half.BOTTOM); + data.setFacing(switch (shape.name()) { + case "STRAIGHT_SOUTH" -> BlockFace.SOUTH; + case "STRAIGHT_EAST", "INNER_NORTH_EAST", "INNER_SOUTH_EAST", "OUTER_NORTH_EAST", + "OUTER_SOUTH_EAST" -> BlockFace.EAST; + case "STRAIGHT_WEST", "INNER_NORTH_WEST", "INNER_SOUTH_WEST", "OUTER_NORTH_WEST", + "OUTER_SOUTH_WEST" -> BlockFace.WEST; + default -> BlockFace.NORTH; + }); + loc.getBlock().setBlockData(data); + } + } + } + + return true; + } + + @Override + public @Nullable List onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command, + @NotNull String s, @NotNull String[] strings) { + return List.of("1", "2", "3", "4"); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/BootedComputer.java b/src/main/java/de/blazemcworld/blazinggames/computing/BootedComputer.java new file mode 100644 index 0000000..988ff16 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/BootedComputer.java @@ -0,0 +1,309 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing; + +import com.caoccao.javet.exceptions.JavetException; +import com.caoccao.javet.interception.logging.JavetStandardConsoleInterceptor; +import com.caoccao.javet.interop.V8Host; +import com.caoccao.javet.interop.V8Runtime; +import com.caoccao.javet.interop.options.V8RuntimeOptions; +import com.caoccao.javet.values.reference.IV8ValueObject; +import com.caoccao.javet.values.reference.V8ValueObject; +import de.blazemcworld.blazinggames.computing.functions.GlobalFunctions; +import de.blazemcworld.blazinggames.computing.functions.JSFunctionalClass; +import de.blazemcworld.blazinggames.computing.motor.IComputerMotor; +import de.blazemcworld.blazinggames.computing.types.ComputerTypes; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; + +public class BootedComputer { + private String code; + + // Static metadata + private final String id; + private final ComputerTypes type; + + // Dynamic metadata + private UUID address; + private String name; + private Location location; + private ArrayList upgrades; + private UUID owner; + private ArrayList collaborators; + private boolean shouldRun; + private int frozenTicks; + + // Runtime data + private DesiredState desiredState = DesiredState.NO_CHANGE; + private boolean wantsStateReset = false; + UUID motorRuntimeEntityUUID; + int motorRuntimeEntityHits = 0; + UUID motorRuntimeEntityHitAttacker = null; + + // State + private V8RuntimeOptions state; + + BootedComputer( + final ComputerMetadata metadata, + final Location location, + final byte[] state, + final String code + ) { + this.id = metadata.id; + this.type = metadata.type; + this.code = code; + this.location = location; + this.address = metadata.address; + this.name = metadata.name; + this.upgrades = new ArrayList<>(List.of(metadata.upgrades)); + upgrades.addAll(List.of(type.getType().getDefaultUpgrades())); // add defaults + upgrades = new ArrayList<>(upgrades.stream().distinct().collect(Collectors.toList())); // remove duplicates + this.owner = metadata.owner; + this.collaborators = new ArrayList<>(List.of(metadata.collaborators)); + this.shouldRun = metadata.shouldRun; + this.frozenTicks = metadata.frozenTicks; + + this.state = new V8RuntimeOptions(); + this.state.setCreateSnapshotEnabled(true); + if (state != null) { + this.state.setSnapshotBlob(state); + } + + IComputerMotor motor = type.getType().getMotor(); + if (motor.usesBlock()) { + location.getBlock().setType(motor.blockMaterial()); + motor.applyPropsToBlock(location.getBlock()); + } + + if (motor.usesActor()) { + Entity entity = location.getWorld().spawnEntity(location, motor.actorEntityType()); + motor.applyActorProperties(entity); + this.motorRuntimeEntityUUID = entity.getUniqueId(); + } + } + + void hibernateNow() { + this.stopCodeExecution(); + IComputerMotor motor = this.type.getType().getMotor(); + if (motor.usesBlock()) { + this.location.getBlock().setType(Material.AIR); + } + + if (motor.usesActor()) { + if (this.motorRuntimeEntityUUID == null) { + return; + } + + Entity entity = this.location.getWorld().getEntity(this.motorRuntimeEntityUUID); + if (entity != null) { + entity.remove(); + } + + this.motorRuntimeEntityUUID = null; + } + + this.location = null; + } + + void saveNow() { + ComputerRegistry.saveToDisk(this); + } + + void tick() { + // switch state if needed + if (this.desiredState == DesiredState.STOPPED) { + this.shouldRun = false; + this.desiredState = DesiredState.NO_CHANGE; + } else if (this.desiredState == DesiredState.RUNNING) { + this.shouldRun = true; + this.desiredState = DesiredState.NO_CHANGE; + } else if (this.desiredState == DesiredState.RESTART) { + this.shouldRun = false; + this.wantsStateReset = true; + this.desiredState = DesiredState.RUNNING; + } + + // reset state if needed + if (this.wantsStateReset) { + this.state.setSnapshotBlob(null); + this.frozenTicks = 0; + this.wantsStateReset = false; + } + + if (this.shouldRun && this.frozenTicks > 0) { + // unfreeze if frozen + this.frozenTicks--; + } else if (this.shouldRun) { + try { + V8Runtime v8Runtime = V8Host.getV8Instance().createV8Runtime(this.state); + + GlobalFunctions functions = new GlobalFunctions(this, v8Runtime); + V8ValueObject v8ValueObject = v8Runtime.createV8ValueObject(); + + try { + v8Runtime.getGlobalObject().set(functions.getNamespace(), v8ValueObject); + v8ValueObject.bind(functions); + } catch (Throwable t) { + if (v8ValueObject != null) { + try { + v8ValueObject.close(); + } catch (Throwable tt) { + t.addSuppressed(tt); + } + } + + throw t; + } + + if (v8ValueObject != null) { + v8ValueObject.close(); + } + + JavetStandardConsoleInterceptor console = new JavetStandardConsoleInterceptor(v8Runtime); + console.register(new IV8ValueObject[]{v8Runtime.getGlobalObject()}); + + for (JSFunctionalClass functionalClass : this.type.getType().getFunctions(this)) { + V8ValueObject v8ValueObjectx = v8Runtime.createV8ValueObject(); + v8Runtime.getGlobalObject().set(functionalClass.getNamespace(), v8ValueObjectx); + v8ValueObjectx.bind(functionalClass); + if (v8ValueObjectx != null) { + v8ValueObjectx.close(); + } + } + + v8Runtime.getExecutor(this.code).executeVoid(); + + if (v8Runtime != null) { + v8Runtime.close(); + } + } catch (JavetException e) { + throw new RuntimeException(e); + } + } + } + + public void startCodeExecution() { + this.desiredState = DesiredState.RUNNING; + } + + public void stopCodeExecution() { + this.desiredState = DesiredState.STOPPED; + } + + public void resetCodeExecutionState() { + this.wantsStateReset = true; + } + + public void restartCodeExecution() { + this.desiredState = DesiredState.RESTART; + } + + public void freeze(int ticks) { + if (this.frozenTicks < 0) { + this.frozenTicks = 0; + } + + this.frozenTicks += ticks; + } + + public ComputerMetadata getMetadata() { + List defaultUpgrades = List.of(type.getType().getDefaultUpgrades()); + return new ComputerMetadata( + this.id, + this.name, + this.address, + this.type, + this.upgrades.stream().filter(upgrade -> !defaultUpgrades.contains(upgrade)).toArray(String[]::new), + this.location, + this.owner, + this.collaborators.toArray(UUID[]::new), + this.shouldRun, + this.frozenTicks + ); + } + + void updateMetadata(ComputerMetadata metadata) { + if (metadata.location.equals(this.location) && metadata.location != null) { + IComputerMotor motor = this.type.getType().getMotor(); + if (motor.usesBlock()) { + this.location.getBlock().setType(Material.AIR); + metadata.location.getBlock().setType(motor.blockMaterial()); + motor.applyPropsToBlock(metadata.location.getBlock()); + } + + if (motor.usesActor()) { + motor.moveActor(this.location.getWorld().getEntity(this.motorRuntimeEntityUUID), metadata.location); + } + } + this.location = metadata.location; + this.address = metadata.address; + this.name = metadata.name; + this.upgrades = new ArrayList<>(List.of(metadata.upgrades)); + upgrades.addAll(List.of(type.getType().getDefaultUpgrades())); // add defaults + upgrades = new ArrayList<>(upgrades.stream().distinct().collect(Collectors.toList())); // remove duplicates + this.owner = metadata.owner; + } + + public byte[] getState() { + return this.state.getSnapshotBlob(); + } + + public String getCode() { + return this.code; + } + + void updateCode(String newCode) { + this.code = newCode; + if (this.shouldRun) { + this.resetCodeExecutionState(); + this.restartCodeExecution(); + } + } + + public void damageHookAddHit(Player attacker) { + this.motorRuntimeEntityHitAttacker = attacker.getUniqueId(); + this.motorRuntimeEntityHits++; + } + + void damageHookRemoveHit() { + if (this.motorRuntimeEntityHits > 0) { + this.motorRuntimeEntityHits--; + } + } + + private static enum DesiredState { + NO_CHANGE, + RUNNING, + STOPPED, + RESTART; + } + + + public String getId() { + return this.id; + } + + public ComputerTypes getType() { + return this.type; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/ComputerEditor.java b/src/main/java/de/blazemcworld/blazinggames/computing/ComputerEditor.java new file mode 100644 index 0000000..6561e6e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/ComputerEditor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +/** + * Utility class for directly modifiying computers. + */ +public class ComputerEditor { + private ComputerEditor() {} + + /** + * Get a list of computers that a user can access. + */ + public static List getAccessibleComputers(final UUID uuid) { + return ComputerRegistry.metadataStorage.query(metadata -> { + if (List.of(metadata.collaborators).contains(uuid)) return true; + return metadata.owner.equals(uuid); + }).stream().map(id -> ComputerRegistry.metadataStorage.getData(id)).toList(); + } + + public static boolean hasAccessToComputer(final UUID user, final String computer) { + ComputerMetadata metadata = ComputerRegistry.metadataStorage.getData(computer); + + if (metadata == null) { + return false; + } + + return metadata.owner == user || Arrays.asList(metadata.collaborators).contains(user); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/ComputerMetadata.java b/src/main/java/de/blazemcworld/blazinggames/computing/ComputerMetadata.java new file mode 100644 index 0000000..09e0821 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/ComputerMetadata.java @@ -0,0 +1,113 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import org.bukkit.Location; + +import com.google.gson.JsonObject; + +import de.blazemcworld.blazinggames.computing.types.ComputerTypes; +import de.blazemcworld.blazinggames.utils.GetGson; +import de.blazemcworld.blazinggames.utils.TextLocation; + +public class ComputerMetadata { + public final String id; + public final String name; + public final UUID address; + public final ComputerTypes type; + public final String[] upgrades; + public final Location location; + public final UUID owner; + public final UUID[] collaborators; + public final boolean shouldRun; + public final int frozenTicks; + + public ComputerMetadata(String id, String name, UUID address, ComputerTypes type, String[] upgrades, + Location location, UUID owner, UUID[] collaborators, boolean shouldRun, int frozenTicks) { + this.id = id; + this.name = name; + this.address = address; + this.type = type; + this.upgrades = upgrades; + this.location = location; + this.owner = owner; + this.collaborators = collaborators; + this.shouldRun = shouldRun; + this.frozenTicks = frozenTicks; + } + + public ComputerMetadata(JsonObject json) { + IllegalArgumentException e = new IllegalArgumentException("Invalid computer metadata"); + this.id = GetGson.getString(json, "id", e); + this.name = GetGson.getString(json, "name", e); + this.address = UUID.fromString(GetGson.getString(json, "address", e)); + this.type = ComputerTypes.valueOf(GetGson.getString(json, "type", e)); + this.upgrades = GetGson.getString(json, "upgrades", e).split(","); + this.location = TextLocation.deserialize(GetGson.getString(json, "location", e)); + this.owner = UUID.fromString(GetGson.getString(json, "owner", e)); + this.collaborators = Arrays.stream(GetGson.getString(json, "collaborators", e).split(",")).map(UUID::fromString).toArray(UUID[]::new); + this.shouldRun = GetGson.getBoolean(json, "shouldRun", e); + this.frozenTicks = GetGson.getNumber(json, "frozenTicks", e).intValue(); + } + + public JsonObject serialize() { + JsonObject object = new JsonObject(); + object.addProperty("id", id); + object.addProperty("name", name); + object.addProperty("address", address.toString()); + object.addProperty("type", type.name()); + object.addProperty("upgrades", String.join(",", upgrades)); + object.addProperty("location", TextLocation.serialize(location)); + object.addProperty("owner", owner.toString()); + object.addProperty("collaborators", String.join(",", Arrays.stream(collaborators).map(UUID::toString).toArray(String[]::new))); + object.addProperty("shouldRun", shouldRun); + object.addProperty("frozenTicks", frozenTicks); + return object; + } + + @Override + public String toString() { + return serialize().toString(); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof ComputerMetadata other) { + return toString().equals(other.toString()); + } + return false; + } + + public boolean hasUpgrade(String upgrade) { + return List.of(upgrades).contains(upgrade); + } + + /** + * Checks if the type of computer being used already has the upgrade being added by default. + */ + public boolean isUpgradePresentForType(String upgrade) { + return List.of(type.getType().getDefaultUpgrades()).contains(upgrade); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/ComputerRegistry.java b/src/main/java/de/blazemcworld/blazinggames/computing/ComputerRegistry.java new file mode 100644 index 0000000..cc55385 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/ComputerRegistry.java @@ -0,0 +1,292 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.types.ComputerTypes; +import de.blazemcworld.blazinggames.computing.types.IComputerType; +import de.blazemcworld.blazinggames.data.DataStorage; +import de.blazemcworld.blazinggames.data.compression.GZipCompressionProvider; +import de.blazemcworld.blazinggames.data.providers.ULIDNameProvider; +import de.blazemcworld.blazinggames.data.storage.BinaryStorageProvider; +import de.blazemcworld.blazinggames.data.storage.GsonStorageProvider; +import de.blazemcworld.blazinggames.data.storage.RawTextStorageProvider; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.utils.NameGenerator; +import de.blazemcworld.blazinggames.utils.Pair; + +import java.util.ArrayList; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +public class ComputerRegistry { + private static final ArrayList computers = new ArrayList<>(); + private static int tick = 0; + private static final int loopOnTick = 100; + private static final int hitsThreshold = 5; + private static final String defaultCode = "// welcome to the editor!\n" + + "// this uses JavaScript along with our custom methods to control computers\n// learn more in the documentation: ______"; + private static final String NAMESPACE = "blazingcomputing"; + public static final NamespacedKey NAMESPACEDKEY_COMPUTER_TYPE = new NamespacedKey(NAMESPACE, "_computer_type"); + public static final NamespacedKey NAMESPACEDKEY_COMPUTER_ID = new NamespacedKey(NAMESPACE, "_computer_id"); + + + public static final DataStorage metadataStorage = DataStorage.forClass( + ComputerRegistry.class, "metadata", + new GsonStorageProvider(ComputerMetadata.class), + new ULIDNameProvider(), new GZipCompressionProvider() + ); + + public static final DataStorage stateStorage = DataStorage.forClass( + ComputerRegistry.class, "state", + new BinaryStorageProvider(), new ULIDNameProvider(), new GZipCompressionProvider() + ); + + public static final DataStorage codeStorage = DataStorage.forClass( + ComputerRegistry.class, "code", + new RawTextStorageProvider("js"), + new ULIDNameProvider(), new GZipCompressionProvider() + ); + + + private ComputerRegistry() {} + + /** + * Load a computer into the world (on server startup) + */ + public static synchronized void loadComputerIntoWorld(final String id) { + final ComputerMetadata metadata = metadataStorage.getData(id); + final byte[] state = stateStorage.getData(id); + final String code = codeStorage.getData(id); + + if (metadata.location == null) return; + if (getComputerById(id) != null) return; + if (getComputerByLocationRounded(metadata.location) != null) return; + + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> { + BootedComputer computer = new BootedComputer(metadata, metadata.location, state, code == null ? defaultCode : code); + computers.add(computer); + }); + } + + /** + * Place an existing computer into the world (on player placing) + */ + public static synchronized void placeComputer(final String id, final Location location, final BiConsumer callback) { + if (getComputerById(id) != null) { + callback.accept(false, getComputerById(id)); + } else if (getComputerByLocationRounded(location) != null) { + callback.accept(false, getComputerByLocationRounded(location)); + } else { + final ComputerMetadata metadata = metadataStorage.getData(id); + final byte[] state = stateStorage.getData(id); + final String code = codeStorage.getData(id); + + if (metadata == null) { + callback.accept(false, null); + return; + } + + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> { + BootedComputer computer = new BootedComputer(metadata, location, state, code == null ? defaultCode : code); + computers.add(computer); + callback.accept(true, computer); + }); + } + } + + /** + * Unload a computer + */ + public static synchronized void unload(final String id) { + BootedComputer computer = getComputerById(id); + if (computer != null) { + computer.hibernateNow(); + saveToDisk(computer); + computers.remove(computer); + } + } + + /** + * Drop the comptuer item in the world. Doesn't break the block, doesn't unload + */ + public static void dropComputer(final BootedComputer computer, final Player player) { + ComputerMetadata metadata = computer.getMetadata(); + ItemStack computerItem = addAttributes(metadata.type.getType().getDisplayItem(computer), metadata.type, computer.getId()); + if (player != null && EnchantmentHelper.hasCustomEnchantment(player.getInventory().getItemInMainHand(), CustomEnchantments.COLLECTABLE)) { + player.getInventory().addItem(new ItemStack[]{computerItem}); + } else { + metadata.location.getWorld().dropItemNaturally(metadata.location, computerItem); + } + } + + /** + * Create a new computer and place it into the world + */ + public static synchronized void placeNewComputer(final Location location, final ComputerTypes type, final UUID ownerUUID, final Consumer callback) { + if (getComputerByLocationRounded(location) != null) { + throw new IllegalArgumentException("Computer already exists at " + location); + } else { + final Pair data = metadataStorage.storeNext((id) -> { + return new ComputerMetadata( + id, + NameGenerator.generateName(), + UUID.randomUUID(), + type, + new String[0], + location, + ownerUUID, + new UUID[0], + false, + 0 + ); + }); + final ComputerMetadata metadata = data.left; + + Bukkit.getScheduler() + .runTask( + BlazingGames.get(), + () -> { + BootedComputer computer = new BootedComputer(metadata, location, null, defaultCode); + computers.add(computer); + callback.accept(computer); + } + ); + } + } + + static synchronized void saveToDisk(BootedComputer computer) { + ComputerMetadata metadata = computer.getMetadata(); + + metadataStorage.storeData(computer.getId(), metadata); + codeStorage.storeData(computer.getId(), computer.getCode()); + + if (computer.getState() != null) { + stateStorage.storeData(computer.getId(), computer.getState()); + } else { + stateStorage.deleteData(computer.getId()); + } + } + + public static BootedComputer getComputerById(String id) { + return computers.stream().filter(computer -> computer.getId().equals(id)).findFirst().orElse(null); + } + + public static BootedComputer getComputerByAddress(UUID address) { + return computers.stream().filter(computer -> computer.getMetadata().address.equals(address)).findFirst().orElse(null); + } + + public static BootedComputer getComputerByLocationExact(Location location) { + return computers.stream().filter(computer -> computer.getMetadata().location.equals(location)).findFirst().orElse(null); + } + + public static BootedComputer getComputerByActorUUID(UUID iAmTiredOfMakingTheseMethods) { + return computers.stream().filter(computer -> iAmTiredOfMakingTheseMethods.equals(computer.motorRuntimeEntityUUID)).findFirst().orElse(null); + } + + public static BootedComputer getComputerByLocationRounded(Location location) { + Location loc = _roundLocation(location); + return computers.stream().filter(computer -> _roundLocation(computer.getMetadata().location).equals(loc)).findFirst().orElse(null); + } + + private static Location _roundLocation(Location loc) { + return new Location(loc.getWorld(), (double) loc.getBlockX(), (double) loc.getBlockY(), (double) loc.getBlockZ()); + } + + public static void tick() { + tick++; + if (tick >= loopOnTick) { + tick = 0; + + for (BootedComputer computer : computers) { + if (computer.motorRuntimeEntityHits >= hitsThreshold) { + Player player = Bukkit.getPlayer(computer.motorRuntimeEntityHitAttacker); + dropComputer(computer, player); + unload(computer.getId()); + } else { + computer.damageHookRemoveHit(); + computer.tick(); + } + } + } else { + for (BootedComputer computerx : computers) { + computerx.tick(); + } + } + } + + public static void registerAllRecipes() { + for (ComputerTypes value : ComputerTypes.values()) { + IComputerType type = value.getType(); + NamespacedKey key = new NamespacedKey(NAMESPACE, value.name().toLowerCase()); + Bukkit.addRecipe(type.getRecipe(key, addAttributes(type.getDisplayItem(null), value.name(), ""))); + } + } + + public static ItemStack addAttributes(ItemStack item, BootedComputer computer) { + return addAttributes(item, computer.getType(), computer.getId()); + } + + public static ItemStack addAttributes(ItemStack item, IComputerType type, String id) { + return addAttributes(item, ComputerTypes.valueOf(type), id); + } + + public static ItemStack addAttributes(ItemStack item, ComputerTypes computerType, String id) { + return addAttributes(item, computerType.name(), id); + } + + public static ItemStack addAttributes(ItemStack item, String computerType, String id) { + ItemStack itemStack = item.clone(); + ItemMeta itemMeta = itemStack.getItemMeta(); + PersistentDataContainer container = itemMeta.getPersistentDataContainer(); + if (computerType != null && !computerType.isEmpty()) { + container.set(NAMESPACEDKEY_COMPUTER_TYPE, PersistentDataType.STRING, computerType); + } + + if (!id.equals("")) { + container.set(NAMESPACEDKEY_COMPUTER_ID, PersistentDataType.STRING, id); + } + + itemStack.setItemMeta(itemMeta); + return itemStack; + } + + public static boolean isComputerItem(ItemStack item) { + if (!item.hasItemMeta()) { + return false; + } else { + ItemMeta itemMeta = item.getItemMeta(); + PersistentDataContainer container = itemMeta.getPersistentDataContainer(); + return !((String)container.getOrDefault(NAMESPACEDKEY_COMPUTER_TYPE, PersistentDataType.STRING, "")).isEmpty(); + } + } + + public static record ComputerPrivileges(boolean chunkloading, boolean network) { + public static ComputerPrivileges minimal() { + return new ComputerPrivileges(false, false); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/APIDocs.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/APIDocs.java new file mode 100644 index 0000000..a353f75 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/APIDocs.java @@ -0,0 +1,190 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public record APIDocs( + String title, + RequestMethod method, + String description, + HashMap incomingArgs, + HashMap outgoingArgs, + HashMap responseCodes, + List permissions +) { + public static APIDocs.Builder builder() { + return new APIDocs.Builder(); + } + + public static class Builder { + private String title = null; + private RequestMethod method = null; + private String description = null; + private final HashMap incomingArgs = new HashMap<>(); + private final HashMap outgoingArgs = new HashMap<>(); + private final HashMap responseCodes = new HashMap<>(); + private final ArrayList permissions = new ArrayList<>(); + + public APIDocs build() { + if (this.title != null && this.method != null && this.description != null) { + return new APIDocs(this.title, this.method, this.description, this.incomingArgs, this.outgoingArgs, this.responseCodes, this.permissions); + } else { + throw new IllegalArgumentException("Missing parameters!"); + } + } + + public String title() { + return this.title; + } + + public APIDocs.Builder title(String title) { + this.title = title; + return this; + } + + public RequestMethod method() { + return this.method; + } + + public APIDocs.Builder method(RequestMethod method) { + this.method = method; + return this; + } + + public String description() { + return this.description; + } + + public APIDocs.Builder description(String description) { + this.description = description; + return this; + } + + public APIDocs.Builder addIncomingArgument(String id, String description) { + this.incomingArgs.put(id, description); + return this; + } + + public APIDocs.Builder removeIncomingArgument(String id) { + this.incomingArgs.remove(id); + return this; + } + + public HashMap incomingArgs() { + return this.incomingArgs; + } + + public APIDocs.Builder addOutgoingArgument(String id, String description) { + this.outgoingArgs.put(id, description); + return this; + } + + public APIDocs.Builder removeOutgoingArgument(String id) { + this.outgoingArgs.remove(id); + return this; + } + + public HashMap outgoingArgs() { + return this.outgoingArgs; + } + + public APIDocs.Builder addResponseCode(int code, String description) { + this.responseCodes.put(code, description); + return this; + } + + public APIDocs.Builder removeResponseCode(int code) { + this.responseCodes.remove(code); + return this; + } + + public HashMap responseCodes() { + return this.responseCodes; + } + + public APIDocs.Builder add200() { + this.responseCodes.put(200, "Success"); + return this; + } + + public APIDocs.Builder add400() { + this.responseCodes.put(400, "Bad request; parameters missing"); + return this; + } + + public APIDocs.Builder add401() { + this.responseCodes.put(401, "Unauthorized; your Authentication header is missing"); + return this; + } + + public APIDocs.Builder add403() { + this.responseCodes.put(403, "Forbidden; insufficient permissions to do this action"); + return this; + } + + public APIDocs.Builder add405() { + this.responseCodes.put(405, "Method not allowed; you are using the wrong request method"); + return this; + } + + public APIDocs.Builder add415() { + this.responseCodes.put(415, "Unsupported Media Type; you are using the wrong content type"); + return this; + } + + public APIDocs.Builder add429() { + this.responseCodes.put(429, "Too Many Requests; you've reached the ratelimit for this endpoint"); + return this; + } + + public APIDocs.Builder add500() { + this.responseCodes.put(500, "Internal server error"); + return this; + } + + public APIDocs.Builder addGenericsUnauthenthicated() { + return this.add200().add400().add500().add405().add429().add415(); + } + + public APIDocs.Builder addGenerics() { + return this.add200().add400().add500().add405().add429().add415().add401().add403(); + } + + public APIDocs.Builder removeBodyFromGenerics() { + return this.removeResponseCode(415); + } + + public APIDocs.Builder addPermission(Permission permission) { + if (!this.permissions.contains(permission)) { + this.permissions.add(permission); + } + + return this; + } + + public APIDocs.Builder removePermission(Permission permission) { + this.permissions.remove(permission); + return this; + } + + public List permissions() { + return List.copyOf(this.permissions); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/BlazingAPIRequestHandler.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/BlazingAPIRequestHandler.java new file mode 100644 index 0000000..1580256 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/BlazingAPIRequestHandler.java @@ -0,0 +1,255 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +import de.blazemcworld.blazinggames.BlazingGames; + +public class BlazingAPIRequestHandler implements HttpHandler { + @Override + public void handle(HttpExchange exchange) throws IOException { + if ((exchange.getRequestURI().getPath().equals("/favicon.ico") || exchange.getRequestURI().getPath().equals("/favicon.ico/")) + && exchange.getRequestMethod().equals("GET")) { + } + + RequestContext context; + try { + context = this.makeContext(exchange); + } catch (Exception e) { + BlazingGames.get().debugLog("Failed to make context (error below)"); + BlazingGames.get().debugLog(e); + throw e; + } + + EndpointResponse response; + try { + if (!ComputingAPI.config.apiConfig().findAt().replace("https://", "").replace("http://", "").equals(context.getFirstHeader("Host"))) { + BlazingGames.get().debugLog("Refused to respond: Host header is " + context.getFirstHeader("Host")); + response = EndpointResponse.builder(421).build(); + } else if (context.ipAddress() == null) { + BlazingGames.get().debugLog("Refused to respond: IP address is null"); + response = EndpointResponse.builder(421).build(); + } else { + response = this.getResponse(context); + } + } catch (Exception e) { + BlazingGames.get().debugLog("Failed to get response"); + BlazingGames.get().debugLog(e); + throw e; + } + + boolean emptyBody; + try { + exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*"); + exchange.getResponseHeaders() + .add("Access-Control-Allow-Methods", Arrays.stream(RequestMethod.values()).map(m -> m.name()).collect(Collectors.joining(", "))); + exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization"); + emptyBody = context.method().flags.contains(RequestMethod.Flag.RESPOND_EMPTY_BODY); + response.headers.forEach((s, s2) -> exchange.getResponseHeaders().add(s, s2)); + exchange.sendResponseHeaders(this.emptyBodyVersionIfNeeded(response.status, emptyBody), (long)response.body.getBytes().length); + } catch (Exception e) { + BlazingGames.get().debugLog("Failed to send response headers"); + BlazingGames.get().debugLog(e); + throw e; + } + + try { + if (!response.body.isEmpty() && !emptyBody) { + try (OutputStream os = exchange.getResponseBody()) { + os.write(response.body.getBytes()); + } + } else { + exchange.getResponseBody().close(); + } + } catch (Exception e) { + BlazingGames.get().debugLog("Failed to send response body (error below)"); + BlazingGames.get().debugLog(e); + throw e; + } + } + + private int emptyBodyVersionIfNeeded(int status, boolean needed) { + if (!needed) { + return status; + } else { + return status == 200 ? 204 : status; + } + } + + private RequestContext makeContext(HttpExchange exchange) { + Map> headers = exchange.getRequestHeaders(); + + RequestMethod method; + try { + method = RequestMethod.valueOf(exchange.getRequestMethod()); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + method = RequestMethod.NULL; + } + + String body; + if (method.flags.contains(RequestMethod.Flag.USE_QUERY_PARAMETERS)) { + body = exchange.getRequestURI().getQuery(); + } else { + try ( + InputStream stream = exchange.getRequestBody(); + InputStreamReader streamReader = new InputStreamReader(stream, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(streamReader); + ) { + StringBuilder builder = new StringBuilder(512); + + int buffer; + while ((buffer = reader.read()) != -1) { + builder.append((char)buffer); + } + + body = builder.toString(); + } catch (IOException e) { + BlazingGames.get().debugLog(e); + body = null; + } + } + + String contentType = headers.getOrDefault("Content-Type", Collections.emptyList()).stream().findFirst().orElse(null); + if (method.flags.contains(RequestMethod.Flag.USE_QUERY_PARAMETERS)) { + contentType = "application/x-www-form-urlencoded"; + } + + if (contentType == null) { + contentType = "application/json"; + } + + contentType = contentType.split(" ")[0].split(";")[0]; + JsonElement jsonBody; + if (body == null) { + jsonBody = null; + } else { + switch (contentType) { + case "application/x-www-form-urlencoded": + JsonObject out = new JsonObject(); + String[] pairs = body.split("&"); + + for (String pair : pairs) { + int idx = pair.indexOf("="); + if (idx != -1) { + out.addProperty( + URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8), + URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8) + ); + } + } + + jsonBody = out; + break; + case "application/json": + try { + jsonBody = JsonParser.parseString(body); + } catch (JsonParseException e) { + jsonBody = null; + BlazingGames.get().debugLog(e); + } + break; + default: + jsonBody = null; + BlazingGames.get().debugLog("No ContentType parser is available for body"); + } + } + + String realIp = exchange.getRemoteAddress().getAddress().getHostAddress(); + String ip; + if (ComputingAPI.config.apiConfig().proxyEnabled()) { + boolean isAllowed = ComputingAPI.config.apiConfig().isAllowed(realIp); + if (headers.containsKey(ComputingAPI.config.apiConfig().proxyIpAddressHeader()) && !headers.get(ComputingAPI.config.apiConfig().proxyIpAddressHeader()).isEmpty()) { + ip = headers.get(ComputingAPI.config.apiConfig().proxyIpAddressHeader()).getFirst(); + } else { + BlazingGames.get().debugLog("Missing IP address header on proxy " + realIp); + ip = null; + } + + if (!isAllowed) { + BlazingGames.get().debugLog("Request from ip " + ip + " via proxy " + realIp + " was denied (not allowed)"); + ip = null; + } + } else { + ip = realIp; + } + + return new RequestContext(jsonBody, headers, ip, exchange.getRequestURI(), method); + } + + private EndpointResponse getResponse(RequestContext context) { + String path = context.uri().getPath(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + + if (path.equals("")) { + path = "/"; + } + + String finalPath = path; + Endpoint endpoint = Arrays.stream(EndpointList.values()) + .filter(ex -> ex.endpoint.path().equals(finalPath)) + .map(ex -> ex.endpoint) + .findFirst() + .orElse(null); + if (endpoint == null) { + return EndpointResponse.of404(); + } else { + try { + String method = context.method().methodCall; + + EndpointResponse response = switch (method) { + case "GET" -> endpoint.GET(context); + case "POST" -> endpoint.POST(context); + case "PUT" -> endpoint.PUT(context); + case "DELETE" -> endpoint.DELETE(context); + case "PATCH" -> endpoint.PATCH(context); + case "OPTIONS" -> EndpointResponse.of204().build(); + default -> EndpointResponse.of405(); + }; + BlazingGames.get().debugLog("Got response " + response.status); + return response; + } catch (EarlyResponse e) { + BlazingGames.get().debugLog("Got early response " + e.getResponse().status); + return e.getResponse(); + } catch (Exception e) { + BlazingGames.get().debugLog("Got exception"); + BlazingGames.get().log(e); + return EndpointResponse.of500(); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/ComputingAPI.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/ComputingAPI.java new file mode 100644 index 0000000..5457b31 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/ComputingAPI.java @@ -0,0 +1,191 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import com.sun.net.httpserver.HttpServer; +import com.sun.net.httpserver.HttpsConfigurator; +import com.sun.net.httpserver.HttpsParameters; +import com.sun.net.httpserver.HttpsServer; +import de.blazemcworld.blazinggames.BlazingGames; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.util.List; +import java.util.regex.Pattern; + +import javax.crypto.SecretKey; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; + +import org.bukkit.configuration.file.FileConfiguration; + +public class ComputingAPI { + static ComputingAPI.Config config = null; + private static HttpServer apiServer; + + private ComputingAPI() { + } + + public static void setConfig(ComputingAPI.Config config) { + ComputingAPI.config = config; + } + + public static ComputingAPI.Config getConfig() { + if (config == null) { + throw new IllegalStateException(); + } else { + return config; + } + } + + public static boolean startAll() { + if (config == null) { + throw new IllegalArgumentException("Config is not defined"); + } else { + if (apiServer != null) { + stopAll(); + } + + // API + apiServer = getConfig().apiConfig.makeServer(); + if (apiServer == null) { return false; } + apiServer.createContext("/", new BlazingAPIRequestHandler()); + apiServer.setExecutor(null); + apiServer.start(); + + + return true; + } + } + + public static void stopAll() { + if (apiServer != null) { + apiServer.stop(15); + } + } + + public static record Config( + boolean spoofMicrosoftServer, + String microsoftClientID, + String microsoftClientSecret, + SecretKey jwtSecretKey, + WebsiteConfig apiConfig, + WebsiteConfig wssConfig + ) {} + + public static record WebsiteConfig( + boolean enabled, + String findAt, + int bindPort, + boolean https, + String httpsPassword, + File httpsFile, + boolean proxyEnabled, + String proxyIpAddressHeader, + boolean proxyAllowAll, + List proxyAllowedIPV4, + List proxyAllowedIPV6 + ) { + public SSLContext makeSSLContext() { + try (FileInputStream fileInputStream = new FileInputStream(this.httpsFile)) { + KeyStore keystore = KeyStore.getInstance("PKCS12"); + keystore.load(fileInputStream, this.httpsPassword.toCharArray()); + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keystore, this.httpsPassword.toCharArray()); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), null, null); + return sslContext; + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | UnrecoverableKeyException | KeyManagementException | IOException e) { + BlazingGames.get().log(e); + return null; + } + } + + public HttpServer makeServer() { + HttpServer server; + + if (this.https) { + try { + HttpsServer https = HttpsServer.create(new InetSocketAddress(this.bindPort), 0); + https.setHttpsConfigurator(new HttpsConfigurator(this.makeSSLContext()) { + @Override + public void configure(HttpsParameters params) { + SSLContext context = this.getSSLContext(); + SSLEngine engine = context.createSSLEngine(); + params.setNeedClientAuth(false); + params.setCipherSuites(engine.getEnabledCipherSuites()); + params.setProtocols(engine.getEnabledProtocols()); + SSLParameters sslParameters = context.getDefaultSSLParameters(); + params.setSSLParameters(sslParameters); + } + }); + server = https; + } catch (IOException e) { + server = null; + BlazingGames.get().log(e); + } + } else { + try { + server = HttpServer.create(new InetSocketAddress(this.bindPort), 0); + } catch (IOException e) { + server = null; + BlazingGames.get().log(e); + } + } + + return server; + } + + public static WebsiteConfig auto(FileConfiguration config, String rootPath) { + return new WebsiteConfig( + config.getBoolean(rootPath + ".enabled"), + config.getString(rootPath + ".find-at"), + config.getInt(rootPath + ".bind.port"), + config.getBoolean(rootPath + ".bind.https.enabled"), + config.getString(rootPath + ".bind.https.password"), + new File(config.getString(rootPath + ".bind.https.file")), + config.getBoolean(rootPath + ".proxy.in-use"), + config.getString(rootPath + ".proxy.ip-address-header"), + config.getBoolean(rootPath + ".proxy.allow-all"), + config.getStringList(rootPath + ".proxy.allowed-ipv4"), + config.getStringList(rootPath + ".proxy.allowed-ipv6") + ); + } + + public boolean isAllowed(String ip) { + if (this.proxyAllowAll) { + return true; + } + if (Pattern.matches("^(?:(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})(?:\\.(?!$)|$)){4}$", ip)) { + return this.proxyAllowedIPV4.contains(ip); + } else { + return Pattern.matches("^((([0-9A-Fa-f]{1,4}:){1,6}:)|(([0-9A-Fa-f]{1,4}:){7}))([0-9A-Fa-f]{1,4})$", ip) + ? this.proxyAllowedIPV6.contains(ip) + : false; + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/DangerLevel.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/DangerLevel.java new file mode 100644 index 0000000..1cbdd66 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/DangerLevel.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +public enum DangerLevel { + LOW("Low"), + MEDIUM("Medium"), + HIGH("High"), + CRITICAL("\u26A0 Danger"); + + public final String display; + + private DangerLevel(String display) { + this.display = display; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/EarlyResponse.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/EarlyResponse.java new file mode 100644 index 0000000..0eeb694 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/EarlyResponse.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +public class EarlyResponse extends Throwable { + private final EndpointResponse response; + + public EarlyResponse(EndpointResponse response) { + this.response = response; + } + + public static EarlyResponse of(EndpointResponse e) { + return new EarlyResponse(e); + } + + public EndpointResponse getResponse() { + return this.response; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/Endpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/Endpoint.java new file mode 100644 index 0000000..65b67ed --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/Endpoint.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +public interface Endpoint { + String path(); + + default EndpointResponse GET(RequestContext context) throws EarlyResponse { + return EndpointResponse.of405(); + } + + default EndpointResponse POST(RequestContext context) throws EarlyResponse { + return EndpointResponse.of405(); + } + + default EndpointResponse PUT(RequestContext context) throws EarlyResponse { + return EndpointResponse.of405(); + } + + default EndpointResponse DELETE(RequestContext context) throws EarlyResponse { + return EndpointResponse.of405(); + } + + default EndpointResponse PATCH(RequestContext context) throws EarlyResponse { + return EndpointResponse.of405(); + } + + APIDocs[] docs(); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/EndpointList.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/EndpointList.java new file mode 100644 index 0000000..14bab6e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/EndpointList.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import de.blazemcworld.blazinggames.computing.api.impl.RootEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthCallbackEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthConsentEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthErrorEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthLinkEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthPrepareEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthRedeemEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthTestEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthUnlinkConfirmEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.auth.AuthUnlinkEndpoint; +import de.blazemcworld.blazinggames.computing.api.impl.computers.ComputersListEndpoint; + +public enum EndpointList { + ROOT(null, new RootEndpoint()), + AUTH_PREPARE("Authenthication", new AuthPrepareEndpoint()), + AUTH_LINK("Authenthication", new AuthLinkEndpoint()), + AUTH_TEST("Authenthication", new AuthTestEndpoint()), + AUTH_REDEEM("Authenthication", new AuthRedeemEndpoint()), + AUTH_CALLBACK(null, new AuthCallbackEndpoint()), + AUTH_CONSENT(null, new AuthConsentEndpoint()), + AUTH_ERROR(null, new AuthErrorEndpoint()), + AUTH_UNLINK(null, new AuthUnlinkEndpoint()), + AUTH_UNLINK_CONFIRM(null, new AuthUnlinkConfirmEndpoint()), + COMPUTER_LIST("Computers", new ComputersListEndpoint()); + + public final String category; + public final Endpoint endpoint; + + private EndpointList(String category, Endpoint endpoint) { + this.endpoint = endpoint; + this.category = category; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/EndpointResponse.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/EndpointResponse.java new file mode 100644 index 0000000..2291b68 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/EndpointResponse.java @@ -0,0 +1,172 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.BlazingGames; +import freemarker.template.Configuration; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.TemplateExceptionHandler; +import java.io.IOException; +import java.io.StringWriter; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.TimeZone; + +public class EndpointResponse { + final int status; + final Map headers; + final String body; + private static final Configuration config = new Configuration(Configuration.VERSION_2_3_33); + + public EndpointResponse(int status, HashMap headers, String body) { + this.status = status; + this.headers = Map.copyOf(headers); + this.body = body == null ? "" : body; + } + + public static EndpointResponse.Builder builder(int status) { + return new EndpointResponse.Builder(status); + } + + private static EndpointResponse.Builder _genericError(int code, String content) { + JsonObject object = new JsonObject(); + object.addProperty("code", code); + object.addProperty("message", content); + object.addProperty("success", false); + return new EndpointResponse.Builder(code).header("Content-Type", "application/json").body(BlazingGames.gson.toJson(object)); + } + + public static EndpointResponse.Builder of200(JsonObject json) { + json.addProperty("success", true); + return new EndpointResponse.Builder(200).header("Content-Type", "application/json").body(BlazingGames.gson.toJson(json)); + } + + public static EndpointResponse.Builder of204() { + return new EndpointResponse.Builder(204); + } + + public static EndpointResponse redirect(String newLocation) { + return new EndpointResponse.Builder(302).header("Location", newLocation).build(); + } + + public static EndpointResponse of400(String error) { + return _genericError(400, error).build(); + } + + public static EndpointResponse of401() { + return _genericError(401, "Unauthorized").build(); + } + + public static EndpointResponse of403() { + return _genericError(403, "Forbidden").build(); + } + + public static EndpointResponse of404() { + return _genericError(404, "Not Found").build(); + } + + public static EndpointResponse of405() { + return _genericError(405, "Method Not Allowed").build(); + } + + public static EndpointResponse of500() { + return _genericError(500, "Internal Server Error").build(); + } + + public static EndpointResponse authError(String title, String desc) { + return redirect("/auth/error?error=" + URLEncoder.encode(title, StandardCharsets.UTF_8) + "&desc=" + URLEncoder.encode(desc, StandardCharsets.UTF_8)); + } + + public static EndpointResponse ofHTML(String templatePath, Map map) { + Map data = map == null ? new HashMap<>() : map; + + try { + Template template = config.getTemplate(templatePath); + StringWriter sw = new StringWriter(); + template.process(data, sw); + sw.flush(); + return new EndpointResponse.Builder(200).header("Content-Type", "text/html").body(sw.toString()).build(); + } catch (TemplateException | IOException e) { + BlazingGames.get().log(e); + return of500(); + } + } + + static { + config.setDefaultEncoding("UTF-8"); + config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER); + config.setLogTemplateExceptions(false); + config.setWrapUncheckedExceptions(true); + config.setFallbackOnNullLoopVariable(false); + config.setSQLDateAndTimeTimeZone(TimeZone.getDefault()); + config.setClassForTemplateLoading(BlazingGames.get().getClass(), "/html/"); + } + + public static class Builder { + private int status; + private HashMap headers = new HashMap<>(); + private String body = null; + + public Builder(int status) { + this.status = status; + } + + public int status() { + return this.status; + } + + public EndpointResponse.Builder status(int status) { + this.status = status; + return this; + } + + public HashMap headers() { + return this.headers; + } + + public EndpointResponse.Builder header(String name, String value) { + this.headers.put(name, value); + return this; + } + + public EndpointResponse.Builder removeHeader(String name) { + this.headers.remove(name); + return this; + } + + public EndpointResponse.Builder headers(HashMap headers) { + this.headers = headers; + return this; + } + + public String body() { + return this.body; + } + + public EndpointResponse.Builder body(String body) { + this.body = body; + return this; + } + + public EndpointResponse build() { + return new EndpointResponse(this.status, this.headers, this.body); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/LinkedUser.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/LinkedUser.java new file mode 100644 index 0000000..53a1701 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/LinkedUser.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.utils.GetGson; +import de.blazemcworld.blazinggames.utils.JWTUtils; + +public record LinkedUser(String username, UUID uuid, int level, long instant, List permissions, long expiresAt) { + public static final String ISSUER = "LNKD"; + + public static LinkedUser deserialize(JsonObject json) { + try { + String name = GetGson.getString(json, "username", new IllegalArgumentException("Missing username property while deserializing LinkedUser")); + String rawUUID = GetGson.getString(json, "uuid", new IllegalArgumentException("Missing uuid property while deserializing LinkedUser")); + int level = GetGson.getNumber(json, "level", new IllegalArgumentException("Missing level property while deserializing LinkedUser")).intValue(); + long instant = GetGson.getNumber(json, "instant", new IllegalArgumentException("Missing instant property while deserializing LinkedUser")).longValue(); + long expiresAt = GetGson.getNumber(json, "expiresAt", new IllegalArgumentException("Missing expiresAt property while deserializing LinkedUser")).longValue(); + + JsonArray rawPermissions = GetGson.getArray( + json, "permissions", new IllegalArgumentException("Missing permissions property while deserializing LinkedUser") + ); + ArrayList permissions = new ArrayList<>(); + for (JsonElement elem : rawPermissions) { + String permission = GetGson.getAsString( + elem, new IllegalArgumentException("Invalid permission while deserializing LinkedUser (not string)") + ); + + permissions.add(Permission.valueOf(permission)); + } + + return new LinkedUser(name, UUID.fromString(rawUUID), level, instant, List.copyOf(permissions), expiresAt); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + public static JsonObject serialize(LinkedUser linked) { + JsonArray permissions = new JsonArray(); + for (Permission p : linked.permissions) { + permissions.add(p.name()); + } + + JsonObject out = new JsonObject(); + out.addProperty("username", linked.username); + out.addProperty("uuid", linked.uuid.toString()); + out.addProperty("level", linked.level); + out.addProperty("instant", linked.instant); + out.addProperty("expiresAt", linked.expiresAt); + out.add("permissions", permissions); + return out; + } + + public static String signLinkedUser(LinkedUser linked) { + long timeUntilExp = TimeUnit.SECONDS.toMillis(linked.expiresAt()) - System.currentTimeMillis(); + if (timeUntilExp < 0) { + timeUntilExp = 0; + } + return JWTUtils.sign(serialize(linked), ISSUER, timeUntilExp); + } + + public static LinkedUser getLinkedUserFromJWT(String jwt) { + JsonObject obj = JWTUtils.parseToJsonObject(jwt, ISSUER); + LinkedUser linked = obj == null ? null : deserialize(obj); + if (linked == null) { return null; } + if (TokenManager.hasConsentRevoked(linked)) { return null; } + return linked; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/Permission.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/Permission.java new file mode 100644 index 0000000..a96f263 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/Permission.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +public enum Permission { + READ_COMPUTERS("Read a list of your computers, with metadata", DangerLevel.LOW), + WRITE_COMPUTERS("Unregister and edit metadata of your computers", DangerLevel.HIGH), + RESTART_COMPUTER("Restart your computers", DangerLevel.LOW), + START_STOP_COMPUTER("Start or stop your computers", DangerLevel.MEDIUM), + COMPUTER_CODE_READ("View your computer code", DangerLevel.LOW), + COMPUTER_CODE_MODIFY("Modify your computer code", DangerLevel.MEDIUM), + COLLABORATOR_MANAGEMENT("Manage collaborators of your computers (with consent)", DangerLevel.MEDIUM), + OWNER_MANAGEMENT("Transfer ownership of your computers (with consent)", DangerLevel.MEDIUM), + COLLABORATOR_OWNER_MANAGEMENT_NO_CONSENT("Do not require consent for collaborator and owner changes", DangerLevel.CRITICAL); + + public final String description; + public final DangerLevel level; + + private Permission(String description, DangerLevel level) { + this.description = description; + this.level = level; + } + + public boolean isAllowed(LinkedUser user) { + return user.permissions().contains(this); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/RequestContext.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/RequestContext.java new file mode 100644 index 0000000..533f6ea --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/RequestContext.java @@ -0,0 +1,108 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import com.google.gson.JsonElement; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +public record RequestContext(JsonElement body, Map> headers, String ipAddress, URI uri, RequestMethod method) { + public static final String CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()!@#()-_=+[]{}|;:'\",./? "; + + public String getFirstHeader(String header) { + if (!this.headers.containsKey(header)) { + return null; + } else { + return this.headers.get(header).isEmpty() ? null : this.headers.get(header).getFirst(); + } + } + + public boolean hasBody() { + return this.body != null && !this.body.isJsonNull(); + } + + public JsonElement requireBody() throws EarlyResponse { + if (!this.hasBody()) { + throw new EarlyResponse(EndpointResponse.of400("No body")); + } else { + return this.body(); + } + } + + public LinkedUser getAuthentication() { + String authHeader = this.getFirstHeader("Authorization"); + if (authHeader == null) { + return null; + } else { + String[] parts = authHeader.split(" "); + if (parts.length != 2) { + return null; + } else if (!"Bearer".equals(parts[0])) { + return null; + } else { + String token = parts[1]; + return LinkedUser.getLinkedUserFromJWT(token); + } + } + } + + public boolean isAuthenticated() { + return this.getAuthentication() != null; + } + + public LinkedUser requireAuthentication() throws EarlyResponse { + LinkedUser linked = this.getAuthentication(); + if (linked == null) { + throw new EarlyResponse(EndpointResponse.of401()); + } else { + return linked; + } + } + + public void requirePermission(Permission permission) throws EarlyResponse { + LinkedUser linked = this.requireAuthentication(); + if (!linked.permissions().contains(permission)) { + throw new EarlyResponse(EndpointResponse.of403()); + } + } + + public String requireClean(String paramName, String in) throws EarlyResponse { + return this.requireCleanCustom(paramName, in, 3, 80); + } + + public String requireCleanLong(String paramName, String in) throws EarlyResponse { + return this.requireCleanCustom(paramName, in, 3, 250); + } + + public String requireCleanCustom(String paramName, String in, int min, int max) throws EarlyResponse { + if (in.length() < min) { + throw new EarlyResponse(EndpointResponse.of400("Parameter " + paramName + " is too short (at least 3 chars)")); + } else if (in.length() > max) { + throw new EarlyResponse(EndpointResponse.of400("Parameter " + paramName + " is too long (at most 80 chars)")); + } else { + for (int i = 0; i < in.length(); i++) { + char c = in.charAt(i); + if ("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789()!@#()-_=+[]{}|;:'\",./? ".indexOf(c) == -1) { + throw new EarlyResponse(EndpointResponse.of400("Parameter " + paramName + " contains invalid chars (" + c + ")")); + } + } + + return in; + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/RequestMethod.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/RequestMethod.java new file mode 100644 index 0000000..f41fe4b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/RequestMethod.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import java.util.List; + +public enum RequestMethod { + GET("GET", RequestMethod.Flag.USE_QUERY_PARAMETERS), + HEAD("GET", RequestMethod.Flag.USE_QUERY_PARAMETERS, RequestMethod.Flag.RESPOND_EMPTY_BODY), + POST("POST"), + PUT("PUT"), + DELETE("DELETE", RequestMethod.Flag.USE_QUERY_PARAMETERS), + PATCH("PATCH"), + OPTIONS("OPTIONS"), + NULL("NULL"); + + public final String methodCall; + public final List flags; + + private RequestMethod(String methodCall, RequestMethod.Flag... flags) { + this.methodCall = methodCall; + this.flags = List.of(flags); + } + + public static enum Flag { + USE_QUERY_PARAMETERS, + RESPOND_EMPTY_BODY; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/TokenManager.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/TokenManager.java new file mode 100644 index 0000000..afd1407 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/TokenManager.java @@ -0,0 +1,204 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.base.Optional; + +import de.blazemcworld.blazinggames.utils.JWTUtils; +import de.blazemcworld.blazinggames.utils.Pair; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +public class TokenManager { + private TokenManager() {} + private static final SecureRandom rng = new SecureRandom(); + + private static final Cache> preAuthKeys = Caffeine.newBuilder() + .maximumSize(1000L) + .expireAfterWrite(Duration.ofMinutes(10L)) + .build(); + private static final Cache unlinkRequests = Caffeine.newBuilder() + .maximumSize(1000L) + .expireAfterWrite(Duration.ofMinutes(10L)) + .build(); + private static final Cache levels = Caffeine.newBuilder().maximumSize(1000L).expireAfterWrite(Duration.ofHours(13L)).build(); + private static final long instant = Instant.now().getEpochSecond(); + + public static boolean hasConsentRevoked(LinkedUser linked) { + return (linked.instant() != getInstant()) || (linked.level() != getLevel(linked.uuid())); + } + + public static long getInstant() { + return instant; + } + + public static int getLevel(UUID uuid) { + return (Integer)Optional.fromNullable((Integer)levels.getIfPresent(uuid)).or(0); + } + + public static void increaseLevel(UUID uuid) { + if (getLevel(uuid) >= 500) { + levels.invalidate(uuid); + } else { + levels.put(uuid, getLevel(uuid) + 1); + } + } + + public static void storeUnlinkRequest(String token, TokenManager.Profile profile) { + unlinkRequests.put(token, profile); + } + + public static TokenManager.Profile getUnlinkRequest(String token) { + return (TokenManager.Profile)unlinkRequests.getIfPresent(token); + } + + public static void removeUnlinkRequest(String token) { + unlinkRequests.invalidate(token); + } + + public static String generateRandomString(int len) { + byte[] bytes = new byte[len]; + rng.nextBytes(bytes); + StringBuilder out = new StringBuilder(); + + for (byte b : bytes) { + out.append(String.format("%02x", b)); + } + + return out.toString(); + } + + public static String startAuthFlow(TokenManager.ApplicationClaim application) { + String code = generateRandomString(4).toUpperCase(); + preAuthKeys.put(code, new Pair<>(new TokenManager.NotStarted(), application)); + return code; + } + + private static TokenManager.AuthState _getAuthState(String code) { + Pair value = (Pair)preAuthKeys.getIfPresent( + code + ); + return value == null ? null : value.left; + } + + public static TokenManager.ApplicationClaim getApplicationClaimFromCode(String code) { + Pair value = (Pair)preAuthKeys.getIfPresent( + code + ); + return value == null ? null : value.right; + } + + public static String makeServerTokenJWT(String code) { + return JWTUtils.sign(code, "AFTM", TimeUnit.MINUTES, 10L); + } + + public static String getCodeFromJWT(String jwt) { + return JWTUtils.parseToString(jwt, "AFTM"); + } + + public static TokenManager.Profile getProfileFromCode(String code) { + TokenManager.AuthState state = _getAuthState(code); + if (state == null) { + return null; + } else if (state instanceof TokenManager.UserRedirectingToDeciding) { + return ((TokenManager.UserRedirectingToDeciding)state).profile(); + } else { + return state instanceof TokenManager.UserDeciding ? ((TokenManager.UserDeciding)state).profile() : null; + } + } + + public static boolean isCodeNotStarted(String code) { + TokenManager.AuthState state = _getAuthState(code); + return state == null ? false : state instanceof TokenManager.NotStarted; + } + + public static boolean isCodeUserLoggingIn(String code) { + TokenManager.AuthState state = _getAuthState(code); + return state == null ? false : state instanceof TokenManager.UserLoggingIn; + } + + public static boolean isCodeUserRedirectingToDeciding(String code) { + TokenManager.AuthState state = _getAuthState(code); + return state == null ? false : state instanceof TokenManager.UserRedirectingToDeciding; + } + + public static boolean isCodeUserDeciding(String code) { + TokenManager.AuthState state = _getAuthState(code); + return state == null ? false : state instanceof TokenManager.UserDeciding; + } + + public static boolean isCodeUserApproved(String code) { + TokenManager.AuthState state = _getAuthState(code); + return state == null ? false : state instanceof TokenManager.UserApproved; + } + + public static LinkedUser invalidateAndReturnLinkedUser(String code) { + TokenManager.AuthState state = _getAuthState(code); + if (state != null && state instanceof TokenManager.UserApproved approved) { + LinkedUser linked = approved.linked(); + preAuthKeys.invalidate(code); + return linked; + } else { + return null; + } + } + + public static void updateCodeAuthState(String code, TokenManager.AuthState state) { + Pair previousValue = (Pair)preAuthKeys.getIfPresent( + code + ); + if (previousValue != null) { + preAuthKeys.put(code, new Pair<>(state, previousValue.right)); + } + } + + public static boolean verifyConfirmationToken(String code, String token) { + String confirmationToken; + if (isCodeUserDeciding(code)) { + TokenManager.AuthState state = _getAuthState(code); + confirmationToken = ((TokenManager.UserDeciding)state).confirmationToken(); + } else { + if (!isCodeUserRedirectingToDeciding(code)) { + return false; + } + + TokenManager.AuthState state = _getAuthState(code); + confirmationToken = ((TokenManager.UserRedirectingToDeciding)state).confirmationToken(); + } + + return token.equals(confirmationToken); + } + + public static record ApplicationClaim(String name, String contact, String purpose, List permissions) { + } + + public static interface AuthState {} + public static record Errored() implements TokenManager.AuthState {} + public static record NotStarted() implements TokenManager.AuthState {} + public static record Profile(String username, UUID uuid) {} + public static record UserApproved(LinkedUser linked) implements TokenManager.AuthState {} + public static record UserDeciding(TokenManager.Profile profile, String confirmationToken) implements TokenManager.AuthState {} + public static record UserDeclined() implements TokenManager.AuthState {} + public static record UserLoggingIn() implements TokenManager.AuthState {} + public static record UserRedirectingToDeciding(TokenManager.Profile profile, String confirmationToken) implements TokenManager.AuthState {} + public static record WaitingForMicrosoft() implements TokenManager.AuthState {} +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/RootEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/RootEndpoint.java new file mode 100644 index 0000000..1a2f8bc --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/RootEndpoint.java @@ -0,0 +1,120 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointList; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.computing.api.RequestMethod; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.stream.Collectors; +import org.bukkit.configuration.file.FileConfiguration; + +public class RootEndpoint implements Endpoint { + private static EndpointResponse RESPONSE; + + @Override + public String path() { + return "/"; + } + + public static void generateDocs() { + HashMap root = new HashMap<>(); + FileConfiguration config = BlazingGames.get().getConfig(); + + HashMap notice = new HashMap<>(); + notice.put("show", config.getBoolean("docs.notice.show")); + notice.put("title", config.getString("docs.notice.title")); + notice.put("description", config.getString("docs.notice.description")); + notice.put("button", config.getString("docs.notice.button-title")); + notice.put("url", config.getString("docs.notice.button-url")); + root.put("notice", notice); + + HashMap instance = new HashMap<>(); + instance.put("label", config.getString("docs.official-instance.name")); + instance.put("url", config.getString("docs.official-instance.url")); + root.put("instance", instance); + + ArrayList> playerurls = new ArrayList<>(); + config.getMapList("docs.user-links").forEach(map -> { + HashMap url = new HashMap<>(); + url.put("label", map.get("name")); + url.put("url", map.get("url")); + playerurls.add(url); + }); + root.put("playerurls", playerurls); + + ArrayList> devurls = new ArrayList<>(); + config.getMapList("docs.developer-links").forEach(map -> { + HashMap url = new HashMap<>(); + url.put("label", map.get("name")); + url.put("url", map.get("url")); + devurls.add(url); + }); + root.put("devurls", devurls); + + HashMap>> endpoints = new HashMap<>(); + for (EndpointList endpoint : EndpointList.values()) { + String category = endpoint.category; + if (category != null) { + if (!endpoints.containsKey(category)) { + endpoints.put(category, new ArrayList<>()); + } + + HashMap entry = new HashMap<>(); + APIDocs[] docs = endpoint.endpoint.docs(); + + for (APIDocs doc : docs) { + entry.put("title", doc.title()); + entry.put("description", doc.description()); + entry.put("path", endpoint.endpoint.path()); + entry.put("method", doc.method().name()); + entry.put("hrefid", "docs-" + doc.title().replace(" ", "-").toLowerCase()); + entry.put("paramstitle", doc.method().flags.contains(RequestMethod.Flag.USE_QUERY_PARAMETERS) ? "Query parameters" : "Request body"); + entry.put("incoming", doc.incomingArgs()); + entry.put("outgoing", doc.outgoingArgs()); + entry.put( + "responsecodes", doc.responseCodes().entrySet().stream().collect(Collectors.toMap(e -> String.valueOf(e.getKey()), Entry::getValue)) + ); + entry.put("permissions", doc.permissions().stream().collect(Collectors.toMap(Enum::name, p -> p.description))); + endpoints.get(category).add(entry); + } + } + } + + root.put("endpointcategories", endpoints); + RESPONSE = EndpointResponse.ofHTML("docs.html", root); + } + + @Override + public EndpointResponse GET(RequestContext context) { + if (RESPONSE == null) { + generateDocs(); + } + + return RESPONSE; + } + + @Override + public APIDocs[] docs() { + return new APIDocs[0]; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthCallbackEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthCallbackEndpoint.java new file mode 100644 index 0000000..82abe51 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthCallbackEndpoint.java @@ -0,0 +1,315 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.TokenManager; +import de.blazemcworld.blazinggames.computing.api.ComputingAPI; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.utils.GetGson; +import java.io.IOException; +import java.util.UUID; +import java.util.regex.Pattern; + +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.FormBody.Builder; + +public class AuthCallbackEndpoint implements Endpoint { + public static final String PATH = "/auth/callback"; + private final OkHttpClient client = new OkHttpClient(); + + @Override + public String path() { + return PATH; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing query parameters"))); + if (body.has("error") && body.has("error_description")) { + JsonElement errorElem = body.get("error"); + String error; + if (errorElem.isJsonPrimitive() && errorElem.getAsJsonPrimitive().isString()) { + error = context.requireClean("error", errorElem.getAsJsonPrimitive().getAsString()); + } else { + error = "Invalid error code provided"; + } + + JsonElement errorDescriptionElem = body.get("error_description"); + String errorDescription; + if (errorDescriptionElem.isJsonPrimitive() && errorDescriptionElem.getAsJsonPrimitive().isString()) { + errorDescription = context.requireClean("error_description", errorDescriptionElem.getAsJsonPrimitive().getAsString()); + } else { + errorDescription = "Invalid error code provided"; + } + + return EndpointResponse.authError(error, errorDescription); + } else { + String msCode = context.requireClean("code", GetGson.getString(body, "code", EarlyResponse.of(EndpointResponse.of400("Missing code argument")))); + String state = GetGson.getString(body, "state", EarlyResponse.of(EndpointResponse.of400("Missing state argument"))); + if (state.length() != 8) { + return EndpointResponse.of400("Invalid state argument"); + } else { + boolean isUnlinkRequest = (state.equals(AuthUnlinkEndpoint.MAGIC_UNLINK_STATE)); + if (!isUnlinkRequest) { + if (!TokenManager.isCodeUserLoggingIn(state)) { + return EndpointResponse.authError("Token (state) is invalid or expired", "The token might've expired after 10 minutes of inactivity"); + } + + TokenManager.updateCodeAuthState(state, new TokenManager.WaitingForMicrosoft()); + } + + TokenManager.Profile profile = this.microsoftAuthenticationDance(msCode); + if (profile == null) { + if (!isUnlinkRequest) { + TokenManager.updateCodeAuthState(state, new TokenManager.Errored()); + } + + if (ComputingAPI.getConfig().spoofMicrosoftServer()) { + return EndpointResponse.authError("Bad username/UUID", "Or you didn't provide any."); + } + + return EndpointResponse.authError( + "An error with Microsoft authenthication occurred", "Make sure that you own Minecraft and have picked a username." + ); + } else { + String confirmationToken = TokenManager.generateRandomString(32); + if (isUnlinkRequest) { + TokenManager.storeUnlinkRequest(confirmationToken, profile); + return EndpointResponse.redirect("/auth/unlink-confirm?token=" + confirmationToken); + } else { + TokenManager.updateCodeAuthState(state, new TokenManager.UserRedirectingToDeciding(profile, confirmationToken)); + return EndpointResponse.redirect("/auth/consent?code=" + state + "&token=" + confirmationToken); + } + } + } + } + } + + private RequestBody _makeMultipartBodyFromJson(JsonElement json) { + Builder builder = new Builder(); + + for (String key : json.getAsJsonObject().keySet()) { + builder.add(key, json.getAsJsonObject().get(key).getAsString()); + } + + return builder.build(); + } + + private JsonElement _post(String url, JsonElement body, boolean formUrlEncoded) { + Request request = new okhttp3.Request.Builder() + .url(url) + .header("Accept", "application/json") + .post(formUrlEncoded ? this._makeMultipartBodyFromJson(body) : RequestBody.create(BlazingGames.gson.toJson(body), MediaType.parse("application/json"))) + .header("Content-Type", formUrlEncoded ? "application/x-www-form-urlencoded" : "application/json") + .build(); + + try { + Response response = this.client.newCall(request).execute(); + if (!response.isSuccessful()) { + BlazingGames.get().debugLog("-------- Failed request log start"); + BlazingGames.get().debugLog("Unexpected reply " + response); + BlazingGames.get().debugLog(response.body().string()); + BlazingGames.get().debugLog("-------- Failed request log end"); + return null; + } + + JsonElement responseJson; + if (response.body() == null) { + throw new IOException("Body is empty"); + } + + String rawBody = response.body().string(); + responseJson = JsonParser.parseString(rawBody); + + if (response != null) { + response.close(); + } + + return responseJson; + } catch (JsonParseException | IOException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + private TokenManager.Profile microsoftAuthenticationDance(String code) { + ComputingAPI.Config config = ComputingAPI.getConfig(); + if (config.spoofMicrosoftServer()) { + String[] parts = code.split("\\."); + if (parts.length != 2) { + BlazingGames.get().debugLog("Bad test username/UUID: bad part count"); + return null; + } + + String username = parts[0]; + if (!Pattern.matches("^[a-zA-Z0-9_]{2,16}$", username)) { + BlazingGames.get().debugLog("Bad test username/UUID: name regex didn't match"); + return null; + } + + UUID uuid; + try { + uuid = UUID.fromString(parts[1]); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog("Bad test username/UUID: illegal uuid"); + return null; + } + + return new TokenManager.Profile(username, uuid); + } + + try { + JsonObject token = new JsonObject(); + token.addProperty("client_id", config.microsoftClientID()); + token.addProperty("client_secret", config.microsoftClientSecret()); + token.addProperty("code", code); + token.addProperty("grant_type", "authorization_code"); + token.addProperty("redirect_uri", config.apiConfig().findAt() + PATH); + JsonElement tokenResponseRaw = this._post("https://login.live.com/oauth20_token.srf", token, true); + if (tokenResponseRaw == null) { + return null; + } else { + JsonObject tokenResponse = GetGson.getAsObject(tokenResponseRaw, new IOException("Token response is not an object")); + String tokenToken = GetGson.getString(tokenResponse, "access_token", new IOException("Token response does not contain access_token")); + JsonObject xboxLive = new JsonObject(); + JsonObject xboxLiveProperties = new JsonObject(); + xboxLiveProperties.addProperty("AuthMethod", "RPS"); + xboxLiveProperties.addProperty("SiteName", "user.auth.xboxlive.com"); + xboxLiveProperties.addProperty("RpsTicket", "d=" + tokenToken); + xboxLive.add("Properties", xboxLiveProperties); + xboxLive.addProperty("RelyingParty", "http://auth.xboxlive.com"); + xboxLive.addProperty("TokenType", "JWT"); + JsonElement xboxLiveResponseRaw = this._post("https://user.auth.xboxlive.com/user/authenticate", xboxLive, false); + if (xboxLiveResponseRaw == null) { + return null; + } else { + JsonObject xboxLiveResponse = GetGson.getAsObject(xboxLiveResponseRaw, new IOException("Xbox Live response is not an object")); + String xboxLiveToken = GetGson.getString(xboxLiveResponse, "Token", new IOException("Xbox Live response does not contain Token")); + JsonObject xboxLiveDisplayClaims = GetGson.getObject( + xboxLiveResponse, "DisplayClaims", new IOException("Xbox Live response does not contain DisplayClaims") + ); + JsonArray xboxLiveXUI = GetGson.getArray( + xboxLiveDisplayClaims, "xui", new IOException("Xbox Live response does not contain DisplayClaims.xui") + ); + if (xboxLiveXUI.isEmpty()) { + throw new IOException("DisplayClaims.xui is empty in Xbox Live response"); + } else { + JsonObject xboxLiveXUIElem = GetGson.getAsObject( + xboxLiveXUI.get(0), new IOException("DisplayClaims.xui[0] is not an object in Xbox Live response") + ); + String userHash = GetGson.getString( + xboxLiveXUIElem, "uhs", new IOException("Xbox Live response does not contain DisplayClaims.xui[0].uhs") + ); + JsonObject xsts = new JsonObject(); + JsonObject xstsProperties = new JsonObject(); + JsonArray xstsUserTokens = new JsonArray(); + xstsUserTokens.add(xboxLiveToken); + xstsProperties.add("UserTokens", xstsUserTokens); + xstsProperties.addProperty("SandboxId", "RETAIL"); + xsts.add("Properties", xstsProperties); + xsts.addProperty("RelyingParty", "rp://api.minecraftservices.com/"); + xsts.addProperty("TokenType", "JWT"); + JsonElement xstsResponseRaw = this._post("https://xsts.auth.xboxlive.com/xsts/authorize", xsts, false); + if (xstsResponseRaw == null) { + return null; + } else { + JsonObject xstsResponse = GetGson.getAsObject(xstsResponseRaw, new IOException("XSTS response is not an object")); + String xstsToken = GetGson.getString(xstsResponse, "Token", new IOException("XSTS response does not contain Token")); + JsonObject minecraft = new JsonObject(); + minecraft.addProperty("identityToken", "XBL3.0 x=%s;%s".formatted(userHash, xstsToken)); + JsonElement minecraftResponseRaw = this._post("https://api.minecraftservices.com/authentication/login_with_xbox", minecraft, false); + if (minecraftResponseRaw == null) { + return null; + } else { + JsonObject minecraftResponse = GetGson.getAsObject(minecraftResponseRaw, new IOException("Minecraft response is not an object")); + String minecraftToken = GetGson.getString( + minecraftResponse, "access_token", new IOException("Minecraft response does not contain access_token") + ); + return this.getMinecraftProfileFromMicrosoftAuthenticationDance(minecraftToken); + } + } + } + } + } + } catch (IOException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + private TokenManager.Profile getMinecraftProfileFromMicrosoftAuthenticationDance(String minecraftToken) { + Request request = new okhttp3.Request.Builder() + .url("https://api.minecraftservices.com/minecraft/profile") + .header("Accept", "application/json") + .header("Authorization", "Bearer " + minecraftToken) + .build(); + + try { + Response response = this.client.newCall(request).execute(); + + TokenManager.Profile profile; + if (!response.isSuccessful()) { + throw new IOException("Unexpected code " + response); + } + + if (response.body() == null) { + throw new IOException("Body is empty"); + } + + String rawRawBody = response.body().string(); + JsonElement rawBody = JsonParser.parseString(rawRawBody); + JsonObject body = GetGson.getAsObject(rawBody, new IOException("Body isn't an object")); + String rawUUID = GetGson.getString(body, "id", new IOException("UUID isn't included in body")); + String username = GetGson.getString(body, "name", new IOException("Username isn't included in body")); + StringBuilder uuidBuffer = new StringBuilder(rawUUID); + uuidBuffer.insert(20, '-'); + uuidBuffer.insert(16, '-'); + uuidBuffer.insert(12, '-'); + uuidBuffer.insert(8, '-'); + UUID uuid = UUID.fromString(uuidBuffer.toString()); + profile = new TokenManager.Profile(username, uuid); + + if (response != null) { + response.close(); + } + + return profile; + } catch (JsonParseException | IOException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + @Override + public APIDocs[] docs() { + return new APIDocs[0]; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthConsentEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthConsentEndpoint.java new file mode 100644 index 0000000..d9ea798 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthConsentEndpoint.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.TokenManager; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.LinkedUser; +import de.blazemcworld.blazinggames.computing.api.Permission; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.utils.GetGson; + +import java.time.Instant; +import java.util.HashMap; +import java.util.concurrent.TimeUnit; + +public class AuthConsentEndpoint implements Endpoint { + @Override + public String path() { + return "/auth/consent"; + } + + @Override + public APIDocs[] docs() { + return new APIDocs[0]; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing query parameters"))); + String code = context.requireClean("code", GetGson.getString(body, "code", EarlyResponse.of(EndpointResponse.of400("Missing code argument")))); + String token = context.requireClean("token", GetGson.getString(body, "token", EarlyResponse.of(EndpointResponse.of400("Missing token argument")))); + if (code.length() != 8) { + return EndpointResponse.of400("That's not a code, silly!"); + } else if (!TokenManager.isCodeUserRedirectingToDeciding(code)) { + return EndpointResponse.of400("Code is invalid"); + } else if (!TokenManager.verifyConfirmationToken(code, token)) { + return EndpointResponse.of400("Token is invalid"); + } else { + TokenManager.Profile profile = TokenManager.getProfileFromCode(code); + TokenManager.updateCodeAuthState(code, new TokenManager.UserDeciding(TokenManager.getProfileFromCode(code), token)); + TokenManager.ApplicationClaim appClaim = TokenManager.getApplicationClaimFromCode(code); + HashMap permissions = new HashMap<>(); + + for (Permission p : appClaim.permissions()) { + permissions.put(p.description, p.level.display); + } + + HashMap params = new HashMap<>(); + params.put("code", code); + params.put("token", token); + params.put("username", profile.username()); + params.put("uuid", profile.uuid().toString()); + params.put("appname", appClaim.name()); + params.put("appcontact", appClaim.contact()); + params.put("apppurpose", appClaim.purpose()); + params.put("permissions", permissions); + return EndpointResponse.ofHTML("consent.html", params); + } + } + + @Override + public EndpointResponse POST(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing body"))); + String code = context.requireClean("code", GetGson.getString(body, "code", EarlyResponse.of(EndpointResponse.of400("Missing code argument")))); + String token = context.requireClean("token", GetGson.getString(body, "token", EarlyResponse.of(EndpointResponse.of400("Missing token argument")))); + boolean verdict = context.requireClean( + "verdict", GetGson.getString(body, "verdict", EarlyResponse.of(EndpointResponse.of400("Missing verdict argument"))) + ).equals("true"); + TokenManager.ApplicationClaim appClaim = TokenManager.getApplicationClaimFromCode(code); + if (!TokenManager.isCodeUserDeciding(code)) { + return EndpointResponse.authError("Code is invalid or expired", "No more details are available"); + } else if (!TokenManager.verifyConfirmationToken(code, token)) { + return EndpointResponse.authError("Token is invalid or expired", "No more details are available"); + } else { + TokenManager.Profile profile = TokenManager.getProfileFromCode(code); + int level = TokenManager.getLevel(profile.uuid()); + TokenManager.updateCodeAuthState( + code, verdict ? new TokenManager.UserApproved(new LinkedUser( + profile.username(), + profile.uuid(), + level, + TokenManager.getInstant(), + appClaim.permissions(), + Instant.now().plusSeconds(TimeUnit.HOURS.toSeconds(6L)).getEpochSecond() + )) : new TokenManager.UserDeclined() + ); + String title = verdict ? appClaim.name() + " was granted access to your computers" : appClaim.name() + " was denied access to your computers"; + String desc = verdict ? "This expires in 6 hours." : "Change your mind? You can always grant access later."; + HashMap out = new HashMap<>(); + out.put("title", title); + out.put("body", desc); + out.put("username", profile.username()); + out.put("uuid", profile.uuid()); + return EndpointResponse.ofHTML("verdict.html", out); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthErrorEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthErrorEndpoint.java new file mode 100644 index 0000000..b947f1a --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthErrorEndpoint.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.utils.GetGson; +import java.util.HashMap; + +public class AuthErrorEndpoint implements Endpoint { + @Override + public String path() { + return "/auth/error"; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing query parameters"))); + String error = context.requireClean("error", GetGson.getString(body, "error", EarlyResponse.of(EndpointResponse.of400("Missing error argument")))); + String desc = context.requireClean("desc", GetGson.getString(body, "desc", EarlyResponse.of(EndpointResponse.of400("Missing desc argument")))); + HashMap params = new HashMap<>(); + params.put("error", error); + params.put("desc", desc); + return EndpointResponse.ofHTML("error.html", params); + } + + @Override + public APIDocs[] docs() { + return new APIDocs[0]; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthLinkEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthLinkEndpoint.java new file mode 100644 index 0000000..6238b0e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthLinkEndpoint.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.TokenManager; +import de.blazemcworld.blazinggames.computing.api.ComputingAPI; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.computing.api.RequestMethod; +import de.blazemcworld.blazinggames.utils.GetGson; +import java.net.URLEncoder; +import java.nio.charset.Charset; +import java.util.HashMap; + +public class AuthLinkEndpoint implements Endpoint { + private static final String BASE_URL = "https://login.live.com/oauth20_authorize.srf"; + private static final String SCOPES = "Xboxlive.signin"; + + @Override + public String path() { + return "/auth/link"; + } + + public static String generateMicrosoftLoginURL(String code) { + ComputingAPI.Config config = ComputingAPI.getConfig(); + return BASE_URL + "?response_type=code&approval_prompt=auto&scope=" + SCOPES + + "&client_id=" + + ComputingAPI.getConfig().microsoftClientID() + + "&redirect_uri=" + + URLEncoder.encode(config.apiConfig().findAt() + AuthCallbackEndpoint.PATH, Charset.defaultCharset()) + + "&state=" + + code; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + boolean invalidCode = false; + if (context.hasBody()) { + JsonObject body = GetGson.getAsObject(context.requireBody(), new IllegalStateException()); + if (body.has("code") && body.get("code").isJsonPrimitive() && body.get("code").getAsJsonPrimitive().isString()) { + String code = body.get("code").getAsString(); + if (code.length() != 8) { + invalidCode = true; + } else { + if (TokenManager.isCodeNotStarted(code.toUpperCase())) { + TokenManager.updateCodeAuthState(code.toUpperCase(), new TokenManager.UserLoggingIn()); + + var config = ComputingAPI.getConfig(); + if (config.spoofMicrosoftServer()) { + String username = GetGson.getString(body, "mcname", EarlyResponse.of(EndpointResponse.of400("Missing mcname argument"))); + String uuid = GetGson.getString(body, "mcuuid", EarlyResponse.of(EndpointResponse.of400("Missing mcuuid argument"))); + + return EndpointResponse.redirect(config.apiConfig().findAt() + AuthCallbackEndpoint.PATH + "?code=" + username + "." + uuid + "&state=" + code); + } + + return EndpointResponse.redirect(generateMicrosoftLoginURL(code.toUpperCase())); + } + + invalidCode = true; + } + } + } + + HashMap data = new HashMap<>(); + data.put("error", invalidCode); + data.put("offline", ComputingAPI.getConfig().spoofMicrosoftServer()); + return EndpointResponse.ofHTML("codeinput.html", data); + } + + @Override + public APIDocs[] docs() { + return new APIDocs[]{ + APIDocs.builder() + .title("Start authentication flow (client-side)") + .method(RequestMethod.GET) + .description("Redirect the user to this page, or ask them to open it. This request should NOT be done by the server.") + .addIncomingArgument("code", "(optional) The code if redirected to make the flow faster.") + .addGenericsUnauthenthicated() + .removeBodyFromGenerics() + .addResponseCode(200, "Success (no code param)") + .addResponseCode(302, "Success (with code param)") + .build() + }; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthPrepareEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthPrepareEndpoint.java new file mode 100644 index 0000000..1158606 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthPrepareEndpoint.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.TokenManager; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.Permission; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.computing.api.RequestMethod; +import de.blazemcworld.blazinggames.utils.GetGson; +import java.util.ArrayList; +import java.util.List; + +public class AuthPrepareEndpoint implements Endpoint { + @Override + public String path() { + return "/auth/prepare"; + } + + @Override + public EndpointResponse POST(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing body"))); + String name = context.requireClean("name", GetGson.getString(body, "name", EarlyResponse.of(EndpointResponse.of400("Missing name argument")))); + String contact = context.requireClean( + "contact", GetGson.getString(body, "contact", EarlyResponse.of(EndpointResponse.of400("Missing contact argument"))) + ); + String purpose = context.requireClean( + "purpose", GetGson.getString(body, "purpose", EarlyResponse.of(EndpointResponse.of400("Missing purpose argument"))) + ); + JsonArray rawPermissions = GetGson.getArray(body, "permissions", EarlyResponse.of(EndpointResponse.of400("Missing permissions argument"))); + ArrayList permissions = new ArrayList<>(); + + for (JsonElement elem : rawPermissions) { + try { + permissions.add( + Permission.valueOf( + context.requireClean("permission", GetGson.getAsString(elem, EarlyResponse.of(EndpointResponse.of400("Permission is not a string")))) + ) + ); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + return EndpointResponse.of400("Unrecognised permission"); + } + } + + TokenManager.ApplicationClaim application = new TokenManager.ApplicationClaim(name, contact, purpose, List.copyOf(permissions)); + String code = TokenManager.startAuthFlow(application); + String key = TokenManager.makeServerTokenJWT(code); + JsonObject output = new JsonObject(); + output.addProperty("code", code); + output.addProperty("key", key); + return EndpointResponse.of200(output).build(); + } + + @Override + public APIDocs[] docs() { + return new APIDocs[]{ + APIDocs.builder() + .title("Prepare authentication flow") + .method(RequestMethod.POST) + .description("Prepares a new authenthication flow.") + .addIncomingArgument("name", "The application name, which will be shown on the consent screen.") + .addIncomingArgument("contact", "How you want to get contacted as the owner of the application.") + .addIncomingArgument("purpose", "What the app does. Should be short and clear.") + .addIncomingArgument("permissions", "A list of valid permissions of what the app needs to do. An empty array is accepted, for identification.") + .addOutgoingArgument("code", "8-char code consisting of uppercase letters and numbers for the user to use. See /auth/link.") + .addOutgoingArgument("key", "Key which will allow you to retrieve the real token after the flow is finished. See /auth/redeem.") + .addGenericsUnauthenthicated() + .build() + }; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthRedeemEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthRedeemEndpoint.java new file mode 100644 index 0000000..19ad6fa --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthRedeemEndpoint.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.TokenManager; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.LinkedUser; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.computing.api.RequestMethod; +import de.blazemcworld.blazinggames.utils.GetGson; + +public class AuthRedeemEndpoint implements Endpoint { + @Override + public EndpointResponse POST(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing body or something ,idk"))); + String key = context.requireCleanLong("key", GetGson.getString(body, "key", EarlyResponse.of(EndpointResponse.of400("Missing key argument")))); + String code = TokenManager.getCodeFromJWT(key); + if (code == null) { + return EndpointResponse.of400("Invalid key"); + } else if (!TokenManager.isCodeUserApproved(code)) { + return EndpointResponse.of400("Key isn't approved"); + } else { + LinkedUser linked = TokenManager.invalidateAndReturnLinkedUser(code); + String signed = LinkedUser.signLinkedUser(linked); + JsonObject output = new JsonObject(); + output.addProperty("token", signed); + output.add("body", LinkedUser.serialize(linked)); + return EndpointResponse.of200(output).build(); + } + } + + @Override + public APIDocs[] docs() { + return new APIDocs[]{ + new APIDocs.Builder() + .title("Redeem auth code for token") + .method(RequestMethod.POST) + .description("After a successful auth flow, you can redeem the key you were given in /auth/prepare for a token.") + .addIncomingArgument("key", "The key to redeem") + .addOutgoingArgument("token", "JWT for usage with the other APIs") + .addOutgoingArgument("body", "The JSON of the JWT. Useful if you cannot parse the JWT, otherwise can be ignored. ") + .addGenericsUnauthenthicated() + .build() + }; + } + + @Override + public String path() { + return "/auth/redeem"; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthTestEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthTestEndpoint.java new file mode 100644 index 0000000..adb1748 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthTestEndpoint.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.computing.api.RequestMethod; + +public class AuthTestEndpoint implements Endpoint { + @Override + public String path() { + return "/auth/test"; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + context.requireAuthentication(); + return EndpointResponse.of200(new JsonObject()).build(); + } + + @Override + public APIDocs[] docs() { + return new APIDocs[]{ + APIDocs.builder() + .title("Test Authenthication") + .description("Verify that your JWT is still valid and can be used to authenthicate") + .method(RequestMethod.GET) + .addGenericsUnauthenthicated() + .removeBodyFromGenerics() + .build() + }; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthUnlinkConfirmEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthUnlinkConfirmEndpoint.java new file mode 100644 index 0000000..7c3833f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthUnlinkConfirmEndpoint.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.TokenManager; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.utils.GetGson; +import java.util.HashMap; + +public class AuthUnlinkConfirmEndpoint implements Endpoint { + @Override + public APIDocs[] docs() { + return new APIDocs[0]; + } + + @Override + public String path() { + return "/auth/unlink-confirm"; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing query parameters"))); + String token = context.requireClean("token", GetGson.getString(body, "token", EarlyResponse.of(EndpointResponse.of400("Missing token argument")))); + TokenManager.Profile profile = TokenManager.getUnlinkRequest(token); + if (profile == null) { + return EndpointResponse.authError("Token is invalid or expired", "Tokens expire after 10 minutes. If you want to start over, visit /auth/link."); + } else { + HashMap map = new HashMap<>(); + map.put("token", token); + map.put("username", profile.username()); + map.put("uuid", profile.uuid().toString()); + return EndpointResponse.ofHTML("unlink.html", map); + } + } + + @Override + public EndpointResponse POST(RequestContext context) throws EarlyResponse { + JsonObject body = GetGson.getAsObject(context.requireBody(), EarlyResponse.of(EndpointResponse.of400("Missing body"))); + String token = context.requireClean("token", GetGson.getString(body, "token", EarlyResponse.of(EndpointResponse.of400("Missing token argument")))); + boolean verdict = context.requireClean( + "verdict", GetGson.getString(body, "verdict", EarlyResponse.of(EndpointResponse.of400("Missing verdict argument"))) + ).equals("true"); + TokenManager.Profile profile = TokenManager.getUnlinkRequest(token); + if (profile == null) { + return EndpointResponse.authError("Token is invalid or expired", "Tokens expire after 10 minutes."); + } else { + TokenManager.removeUnlinkRequest(token); + if (verdict) { + TokenManager.increaseLevel(profile.uuid()); + } + + String title = verdict ? "Applications unlinked" : "Canceled unlink request"; + String desc = verdict ? "All currently linked applications are now no longer linked." : "You can always unlink if you want."; + HashMap out = new HashMap<>(); + out.put("title", title); + out.put("body", desc); + out.put("username", profile.username()); + out.put("uuid", profile.uuid()); + return EndpointResponse.ofHTML("verdict.html", out); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthUnlinkEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthUnlinkEndpoint.java new file mode 100644 index 0000000..1fca87d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/auth/AuthUnlinkEndpoint.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.auth; + +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.RequestContext; + +public class AuthUnlinkEndpoint implements Endpoint { + public static final String MAGIC_UNLINK_STATE = "-ULINK--"; + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + return EndpointResponse.redirect(AuthLinkEndpoint.generateMicrosoftLoginURL(MAGIC_UNLINK_STATE)); + } + + @Override + public APIDocs[] docs() { + return new APIDocs[0]; + } + + @Override + public String path() { + return "/auth/unlink"; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/computers/ComputersListEndpoint.java b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/computers/ComputersListEndpoint.java new file mode 100644 index 0000000..ba18fb7 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/api/impl/computers/ComputersListEndpoint.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.api.impl.computers; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import de.blazemcworld.blazinggames.computing.ComputerEditor; +import de.blazemcworld.blazinggames.computing.ComputerMetadata; +import de.blazemcworld.blazinggames.computing.api.APIDocs; +import de.blazemcworld.blazinggames.computing.api.EarlyResponse; +import de.blazemcworld.blazinggames.computing.api.Endpoint; +import de.blazemcworld.blazinggames.computing.api.EndpointResponse; +import de.blazemcworld.blazinggames.computing.api.LinkedUser; +import de.blazemcworld.blazinggames.computing.api.Permission; +import de.blazemcworld.blazinggames.computing.api.RequestContext; +import de.blazemcworld.blazinggames.computing.api.RequestMethod; + +public class ComputersListEndpoint implements Endpoint { + @Override + public String path() { + return "/computers/list"; + } + + @Override + public EndpointResponse GET(RequestContext context) throws EarlyResponse { + LinkedUser linked = context.requireAuthentication(); + context.requirePermission(Permission.READ_COMPUTERS); + JsonObject object = new JsonObject(); + JsonArray computers = new JsonArray(); + + for (ComputerMetadata computer : ComputerEditor.getAccessibleComputers(linked.uuid())) { + computers.add(computer.serialize()); + } + + object.add("computers", computers); + return EndpointResponse.of200(object).build(); + } + + @Override + public APIDocs[] docs() { + return new APIDocs[]{ + APIDocs.builder() + .title("List computers") + .description("View a list of all computers that the player owns or has permissions on") + .method(RequestMethod.GET) + .addGenerics() + .removeBodyFromGenerics() + .addPermission(Permission.READ_COMPUTERS) + .addOutgoingArgument( + "computers", + "List of computers as an array containing objects with properties: id, name, address, type, upgrades, location, running, owner" + ) + .build() + }; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/functions/GlobalFunctions.java b/src/main/java/de/blazemcworld/blazinggames/computing/functions/GlobalFunctions.java new file mode 100644 index 0000000..4a71e02 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/functions/GlobalFunctions.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.functions; + +import com.caoccao.javet.annotations.V8Function; +import com.caoccao.javet.exceptions.JavetException; +import com.caoccao.javet.interop.V8Runtime; +import de.blazemcworld.blazinggames.computing.BootedComputer; +import de.blazemcworld.blazinggames.computing.functions.annotations.MethodDoc; +import de.blazemcworld.blazinggames.computing.functions.annotations.ParamDoc; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; + +public class GlobalFunctions extends JSFunctionalClass { + private final V8Runtime runtime; + + public GlobalFunctions(BootedComputer computer, V8Runtime runtime) { + super(computer); + this.runtime = runtime; + } + + @Override + public String getNamespace() { + return "system"; + } + + @V8Function + public void debugBroadcast(String message) { + Bukkit.broadcast(Component.text(message)); + } + + @V8Function + @MethodDoc("Freezes the computer for the given amount of ticks") + @ParamDoc(param = "ticks", doc = "The amount of ticks to freeze the computer for") + public void freeze(int ticks) { + if (ticks >= 1) { + this.computer.freeze(ticks); + + byte[] snapshot; + try { + snapshot = this.runtime.createSnapshot(); + } catch (JavetException e) { + throw new RuntimeException(e); + } + + this.runtime.terminateExecution(); + if (snapshot != null) { + this.runtime.getRuntimeOptions().setSnapshotBlob(snapshot); + } + } + } + + @V8Function + @MethodDoc("Stops the computer") + public void exit() { + this.computer.stopCodeExecution(); + this.runtime.terminateExecution(); + } + + @V8Function + @MethodDoc("Restarts the computer") + public void restart() { + this.computer.restartCodeExecution(); + this.runtime.terminateExecution(); + } + + @V8Function + @MethodDoc("Freezes the computer for the given amount of ticks and restarts the computer") + @ParamDoc(param = "ticks", doc = "The amount of ticks to freeze the computer for") + public void freezeAndRestart(int ticks) { + this.computer.restartCodeExecution(); + freeze(ticks); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/functions/JSFunctionalClass.java b/src/main/java/de/blazemcworld/blazinggames/computing/functions/JSFunctionalClass.java new file mode 100644 index 0000000..c3a0a30 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/functions/JSFunctionalClass.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.functions; + +import de.blazemcworld.blazinggames.computing.BootedComputer; + +public abstract class JSFunctionalClass { + protected final BootedComputer computer; + + public JSFunctionalClass(BootedComputer computer) { + this.computer = computer; + } + + public abstract String getNamespace(); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/functions/annotations/MethodDoc.java b/src/main/java/de/blazemcworld/blazinggames/computing/functions/annotations/MethodDoc.java new file mode 100644 index 0000000..83a2191 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/functions/annotations/MethodDoc.java @@ -0,0 +1,20 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.functions.annotations; + +public @interface MethodDoc { + String value(); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/functions/annotations/ParamDoc.java b/src/main/java/de/blazemcworld/blazinggames/computing/functions/annotations/ParamDoc.java new file mode 100644 index 0000000..bd35276 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/functions/annotations/ParamDoc.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.functions.annotations; + +public @interface ParamDoc { + String param(); + String doc(); + boolean nullable() default false; +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/motor/HeadComputerMotor.java b/src/main/java/de/blazemcworld/blazinggames/computing/motor/HeadComputerMotor.java new file mode 100644 index 0000000..2f6cd3c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/motor/HeadComputerMotor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.motor; + +import com.destroystokyo.paper.profile.PlayerProfile; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.block.Skull; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +public class HeadComputerMotor implements IComputerMotor { + public final PlayerProfile profile; + + public HeadComputerMotor(PlayerProfile playerProfile) { + this.profile = playerProfile; + } + + @Override + public boolean usesActor() { + return false; + } + + @Override + public EntityType actorEntityType() { + return null; + } + + @Override + public void applyActorProperties(Entity actor) { + } + + @Override + public void moveActor(Entity actor, Location newLocation) { + } + + @Override + public boolean usesBlock() { + return true; + } + + @Override + public Material blockMaterial() { + return Material.PLAYER_HEAD; + } + + @Override + public void applyPropsToBlock(Block block) { + Skull state = (Skull)block.getState(); + state.setPlayerProfile(this.profile); + state.update(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/motor/IComputerMotor.java b/src/main/java/de/blazemcworld/blazinggames/computing/motor/IComputerMotor.java new file mode 100644 index 0000000..d6e6405 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/motor/IComputerMotor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.motor; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; + +public interface IComputerMotor { + boolean usesActor(); + + EntityType actorEntityType(); + + void applyActorProperties(Entity actor); + + void moveActor(Entity actor, Location newLocation); + + boolean usesBlock(); + + Material blockMaterial(); + + void applyPropsToBlock(Block block); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/types/ComputerTypes.java b/src/main/java/de/blazemcworld/blazinggames/computing/types/ComputerTypes.java new file mode 100644 index 0000000..3b7ad83 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/types/ComputerTypes.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.types; + +import java.util.Arrays; +import java.util.function.Supplier; + +import com.google.gson.annotations.SerializedName; + +public enum ComputerTypes { + @SerializedName("CONSOLE") + CONSOLE(ConsoleCT::new, ConsoleCT.class); + + private final Supplier type; + private final Class clazz; + + private ComputerTypes(Supplier type, Class clazz) { + this.type = type; + this.clazz = clazz; + } + + public IComputerType getType() { + return this.type.get(); + } + + public static ComputerTypes valueOf(IComputerType computerType) { + return Arrays.stream(values()).filter(type -> type.clazz.equals(computerType.getClass())).findFirst().orElse(null); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/types/ConsoleCT.java b/src/main/java/de/blazemcworld/blazinggames/computing/types/ConsoleCT.java new file mode 100644 index 0000000..49f4c8f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/types/ConsoleCT.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.types; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.destroystokyo.paper.profile.ProfileProperty; +import de.blazemcworld.blazinggames.computing.BootedComputer; +import de.blazemcworld.blazinggames.computing.functions.JSFunctionalClass; +import de.blazemcworld.blazinggames.computing.motor.HeadComputerMotor; +import de.blazemcworld.blazinggames.computing.motor.IComputerMotor; +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.CraftingRecipe; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.meta.SkullMeta; + +public class ConsoleCT implements IComputerType { + public static final String MINESKIN_USERNAME = "Computer"; + public static final UUID MINESKIN_UUID = UUID.fromString("ea238963-0dbe-45dd-b5a3-6c44da1e57c4"); + public static final String MINESKIN_TEXTURE = "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjBmMTBlODU0MThlMzM0ZjgyNjczZWI0OTQwYjIwOGVjYWVlMGM5NWMyODc2ODVlOWVhZjI0NzUxYTMxNWJmYSJ9fX0="; + + public static PlayerProfile makeProfile() { + PlayerProfile profile = Bukkit.createProfile(MINESKIN_UUID, MINESKIN_USERNAME); + profile.setProperty(new ProfileProperty("textures", MINESKIN_TEXTURE)); + return profile; + } + + @Override + public ItemStack getDisplayItem(BootedComputer computer) { + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta)item.getItemMeta(); + meta.setPlayerProfile(makeProfile()); + meta.displayName(((TextComponent)Component.text("Console").color(NamedTextColor.WHITE)).decoration(TextDecoration.ITALIC, false)); + item.setItemMeta(meta); + return item; + } + + @Override + public CraftingRecipe getRecipe(NamespacedKey key, ItemStack result) { + ShapedRecipe recipe = new ShapedRecipe(key, result); + recipe.shape(new String[]{"III", "IRI", "III"}); + recipe.setIngredient('I', Material.IRON_INGOT); + recipe.setIngredient('R', Material.REDSTONE_BLOCK); + return recipe; + } + + @Override + public IComputerMotor getMotor() { + return new HeadComputerMotor(makeProfile()); + } + + @Override + public JSFunctionalClass[] getFunctions(BootedComputer computer) { + return new JSFunctionalClass[0]; + } + + @Override + public String[] getDefaultUpgrades() { + return new String[0]; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/types/IComputerType.java b/src/main/java/de/blazemcworld/blazinggames/computing/types/IComputerType.java new file mode 100644 index 0000000..b1010e1 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/types/IComputerType.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.types; + +import de.blazemcworld.blazinggames.computing.BootedComputer; +import de.blazemcworld.blazinggames.computing.functions.JSFunctionalClass; +import de.blazemcworld.blazinggames.computing.motor.IComputerMotor; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.CraftingRecipe; +import org.bukkit.inventory.ItemStack; + +public interface IComputerType { + ItemStack getDisplayItem(BootedComputer computer); + + CraftingRecipe getRecipe(NamespacedKey key, ItemStack result); + + IComputerMotor getMotor(); + + JSFunctionalClass[] getFunctions(BootedComputer computer); + + String[] getDefaultUpgrades(); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/wss/BlazingWSS.java b/src/main/java/de/blazemcworld/blazinggames/computing/wss/BlazingWSS.java new file mode 100644 index 0000000..19d8bde --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/wss/BlazingWSS.java @@ -0,0 +1,221 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.wss; + +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.UUID; + +import org.java_websocket.WebSocket; +import org.java_websocket.drafts.Draft; +import org.java_websocket.exceptions.InvalidDataException; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.handshake.ServerHandshakeBuilder; +import org.java_websocket.server.DefaultSSLWebSocketServerFactory; +import org.java_websocket.server.WebSocketServer; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.ComputerEditor; +import de.blazemcworld.blazinggames.computing.api.ComputingAPI; +import de.blazemcworld.blazinggames.computing.api.LinkedUser; +import de.blazemcworld.blazinggames.computing.api.Permission; +import de.blazemcworld.blazinggames.utils.GZipToolkit; +import de.blazemcworld.blazinggames.utils.GetGson; +import de.blazemcworld.blazinggames.utils.Pair; + +public class BlazingWSS extends WebSocketServer { + public static final int ROOM_CLOSE_OR_LEAVE = 1000; + public static final int SHUTDOWN = 1001; + public static final int TOO_LARGE = 1009; + public static final int INTERNAL_ERROR = 1011; + public static final int BAD_GATEWAY = 1014; + public static final int UNAUTHORIZED = 3000; + public static final int FORBIDDEN = 3003; + public static final int KEEPALIVE_TIMEOUT = 3008; + + public static final Permission[] REQUIRED_PERMISSIONS = { + Permission.READ_COMPUTERS, + Permission.COMPUTER_CODE_READ, + Permission.COMPUTER_CODE_MODIFY + }; + + private final ComputingAPI.WebsiteConfig wssConfig; + public BlazingWSS(ComputingAPI.WebsiteConfig wssConfig) { + super(new InetSocketAddress(wssConfig.bindPort())); + this.wssConfig = wssConfig; + if (wssConfig.https()) this.setWebSocketFactory(new DefaultSSLWebSocketServerFactory(wssConfig.makeSSLContext())); + } + + public static Pair decompress(byte[] message) { + try { + String decompressed = GZipToolkit.decompress(message); + JsonObject object = GetGson.getAsObject(JsonParser.parseString(decompressed), new IllegalArgumentException("Invalid JSON (not an object)")); + + String type = GetGson.getString(object, "type", new IllegalArgumentException("Missing type property")); + JsonObject payload = GetGson.getObject(object, "payload", new IllegalArgumentException("Missing payload property")); + return new Pair<>(type, payload); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + public static byte[] compress(String type, JsonObject object) { + JsonObject output = new JsonObject(); + output.addProperty("type", type); + output.add("payload", output); + return GZipToolkit.compress(output.toString()); + } + + public void sendToConn(WebSocket conn, ClientboundPacket packet) { + sendToConn(conn, compress(packet.typeId, packet.serialize())); + } + + public void sendToConn(WebSocket conn, byte[] message) { + ConnectedUser user = conn.getAttachment(); + if (user.disconnectAt > Instant.now().getEpochSecond()) { + conn.close(UNAUTHORIZED); + } else { + conn.send(message); + } + } + + public void sendToRoom(String room, ClientboundPacket packet) { + sendToRoom(room, compress(packet.typeId, packet.serialize())); + } + + public void sendToRoom(String room, byte[] message) { + sendToRoomExcept(room, null, message); + } + + public void sendToRoomExcept(String room, UUID except, ClientboundPacket packet) { + sendToRoomExcept(room, except, compress(packet.typeId, packet.serialize())); + } + + public void sendToRoomExcept(String room, UUID except, byte[] message) { + for (WebSocket conn : getConnections()) { + if (conn.getAttachment() instanceof ConnectedUser connectedUser && connectedUser.room == room && connectedUser.uuid != except) { + sendToConn(conn, message); + } + } + } + + @Override + public void onOpen(WebSocket conn, ClientHandshake handshake) { + // Get IP + Object ipObj = conn.getAttachment(); + if (ipObj == null || !(ipObj instanceof String ip)) { conn.close(BAD_GATEWAY); return; } + + // Get authorization + if (!handshake.hasFieldValue("Authorization")) { conn.close(UNAUTHORIZED); return; } + String authorization = handshake.getFieldValue("Authorization"); + String[] parts = authorization.split(" "); + if (parts.length != 2 || !("Bearer".equals(parts[0]))) { conn.close(UNAUTHORIZED); return; } + String bearerToken = parts[1]; + LinkedUser linked = LinkedUser.getLinkedUserFromJWT(bearerToken); + if (linked == null) { conn.close(UNAUTHORIZED); return; } + + // Get computer id + if (!handshake.hasFieldValue("Blazing-Computer-Id")) { conn.close(FORBIDDEN); return; } + String computerId = handshake.getFieldValue("Blazing-Computer-Id"); + + // Verify permissions + if (!ComputerEditor.hasAccessToComputer(linked.uuid(), computerId)) { conn.close(FORBIDDEN); return; } + for (Permission p : REQUIRED_PERMISSIONS) { + if (!linked.permissions().contains(p)) { conn.close(FORBIDDEN); return; } + } + + // Add attachment + conn.setAttachment(new ConnectedUser(linked.uuid(), linked.username(), linked.level(), linked.expiresAt(), computerId, ip)); + + // Send packets + sendToRoomExcept(computerId, linked.uuid(), new ClientboundPacket.UserListUpdatePacket(linked.uuid(), true)); + } + + @Override + public void onClose(WebSocket conn, int code, String reason, boolean remote) { + ConnectedUser user = conn.getAttachment(); + sendToRoom(user.room(), new ClientboundPacket.UserListUpdatePacket(user.uuid(), false)); + } + + @Override + public void onMessage(WebSocket conn, String message) { + Pair data = decompress(message.getBytes(StandardCharsets.UTF_8)); + ConnectedUser user = conn.getAttachment(); + String computerId = user.room; + + if (user.disconnectAt > Instant.now().getEpochSecond()) { + conn.close(UNAUTHORIZED); + } else if (data != null) { + String type = data.left; + JsonObject payload = data.right; + ServerboundPacket packet; + try { + packet = ServerboundPacket.Type.valueOf(type.toUpperCase()).factory.apply(payload); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + return; + } + packet.process(this, conn, user, computerId); + } + } + + @Override + public void onError(WebSocket conn, Exception ex) { + BlazingGames.get().debugLog(ex); + if (ex instanceof RuntimeException && conn != null && conn.isOpen()) { + conn.close(INTERNAL_ERROR); + } + } + + @Override + public void onStart() { + BlazingGames.get().log("Websocket server started on port " + getPort()); + } + + @Override + public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft, ClientHandshake request) throws InvalidDataException { + ServerHandshakeBuilder builder = super.onWebsocketHandshakeReceivedAsServer(conn, draft, request); + builder.put("Access-Control-Allow-Origin", "*"); + builder.put("Access-Control-Allow-Headers", "Authorization, Blazing-Computer-Id"); + + String realIp = conn.getRemoteSocketAddress().getAddress().getHostAddress(); + String ip; + if (wssConfig.proxyEnabled()) { + if (!wssConfig.isAllowed(realIp)) { + BlazingGames.get().debugLog("IP not allowed: " + realIp); + throw new InvalidDataException(BAD_GATEWAY); + } + + ip = request.getFieldValue(wssConfig.proxyIpAddressHeader()); + if (ip == null) { + BlazingGames.get().debugLog("Proxy header not found: " + wssConfig.proxyIpAddressHeader()); + throw new InvalidDataException(BAD_GATEWAY); + } + } else { + ip = realIp; + } + + conn.setAttachment(ip); + return builder; + } + + public static record ConnectedUser(UUID uuid, String username, int level, long disconnectAt, String room, String ipAddr) {} +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/wss/ClientboundPacket.java b/src/main/java/de/blazemcworld/blazinggames/computing/wss/ClientboundPacket.java new file mode 100644 index 0000000..6fd2712 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/wss/ClientboundPacket.java @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.wss; + +import java.util.UUID; + +import com.google.gson.JsonObject; + +public abstract class ClientboundPacket { + public ClientboundPacket(String typeId) { this.typeId = typeId; } + public final String typeId; + public abstract JsonObject serialize(); + + public static class UserListUpdatePacket extends ClientboundPacket { + public final UUID uuid; + public final boolean isJoin; + public UserListUpdatePacket(UUID uuid, boolean isJoin) { + super("usrupd"); + this.uuid = uuid; + this.isJoin = isJoin; + } + + @Override + public JsonObject serialize() { + JsonObject out = new JsonObject(); + out.addProperty("actioner", uuid.toString()); + out.addProperty("type", isJoin); + return out; + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/computing/wss/ServerboundPacket.java b/src/main/java/de/blazemcworld/blazinggames/computing/wss/ServerboundPacket.java new file mode 100644 index 0000000..90d7aa0 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/computing/wss/ServerboundPacket.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.computing.wss; + +import java.util.function.Function; + +import org.java_websocket.WebSocket; + +import com.google.gson.JsonObject; + +import de.blazemcworld.blazinggames.computing.wss.BlazingWSS.ConnectedUser; + +public abstract class ServerboundPacket { + protected final JsonObject object; + public ServerboundPacket(JsonObject object) { + this.object = object; + } + + public abstract void process(BlazingWSS wss, WebSocket conn, ConnectedUser user, String computerId); + + public static enum Type { + ; + + public final Function factory; + Type(Function factory) { + this.factory = factory; + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/crates/CrateData.java b/src/main/java/de/blazemcworld/blazinggames/crates/CrateData.java new file mode 100644 index 0000000..09ee619 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/crates/CrateData.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.crates; + +import java.util.List; +import java.util.UUID; + +import org.bukkit.Location; + +import org.bukkit.inventory.ItemStack; + +public class CrateData { + public final String id; + public final UUID owner; + public final boolean opened; + public final Location location; + public final int exp; + + public final ItemStack helmet; + public final ItemStack chestplate; + public final ItemStack leggings; + public final ItemStack boots; + public final ItemStack offhand; + + public final List hotbarItems; + public final List inventoryItems; + + public CrateData(String id, UUID owner, boolean opened, Location location, int exp, + ItemStack helmet, ItemStack chestplate, ItemStack leggings, ItemStack boots, ItemStack offhand, + List hotbarItems, List inventoryItems) { + this.id = id; + this.owner = owner; + this.opened = opened; + this.location = location; + this.exp = exp; + this.helmet = helmet; + this.chestplate = chestplate; + this.leggings = leggings; + this.boots = boots; + this.offhand = offhand; + this.hotbarItems = hotbarItems; + this.inventoryItems = inventoryItems; + } +} + diff --git a/src/main/java/de/blazemcworld/blazinggames/crates/CrateManager.java b/src/main/java/de/blazemcworld/blazinggames/crates/CrateManager.java new file mode 100644 index 0000000..b5f0079 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/crates/CrateManager.java @@ -0,0 +1,134 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.crates; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.data.DataStorage; +import de.blazemcworld.blazinggames.data.compression.GZipCompressionProvider; +import de.blazemcworld.blazinggames.data.providers.ULIDNameProvider; +import de.blazemcworld.blazinggames.data.storage.GsonStorageProvider; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +public class CrateManager { + private CrateManager() {} + private static final NamespacedKey KEY = BlazingGames.get().key("death_crate_key"); + private static final DataStorage crateStorage = DataStorage.forClass( + CrateManager.class, null, + new GsonStorageProvider<>(CrateData.class), new ULIDNameProvider(), new GZipCompressionProvider() + ); + + private static boolean shouldStayOnDeath(ItemStack item) { + if (item == null || item.isEmpty()) { + return false; + } + + if (!item.hasItemMeta()) { + return true; + } + + ItemMeta meta = item.getItemMeta(); + return !meta.getEnchants().containsKey(Enchantment.VANISHING_CURSE); + } + + public static String getKeyULID(Location loc) { + List ids = crateStorage.query(data -> { + return !data.opened && + data.location.getWorld().getName().equals(loc.getWorld().getName()) && + data.location.blockX() == loc.getBlockX() && + data.location.blockY() == loc.getBlockY() && + data.location.blockZ() == loc.getBlockZ(); + }); + + if (ids.isEmpty()) { + return null; + } + + if (ids.size() > 1) { + // sort the ULIDs to find the newest + ids.sort((a, b) -> { + return b.compareTo(a); + }); + } + + return ids.get(0); + } + + public static String createDeathCrate(UUID owner, PlayerInventory inventory, int exp, Location crateLocation) { + ArrayList items = new ArrayList<>(); + for (ItemStack item : inventory.getStorageContents()) { + items.add(item); + } + + List hotbarItems = items.subList(0, 9).stream().filter(i -> i == null ? true : CrateManager.shouldStayOnDeath(i)).toList(); + List inventoryItems = items.subList(9, 36).stream().filter(i -> i == null ? true : CrateManager.shouldStayOnDeath(i)).toList(); + + return crateStorage.storeNext(id -> new CrateData( + id, owner, false, + crateLocation, exp, + shouldStayOnDeath(inventory.getHelmet()) ? inventory.getHelmet() : null, + shouldStayOnDeath(inventory.getChestplate()) ? inventory.getChestplate() : null, + shouldStayOnDeath(inventory.getLeggings()) ? inventory.getLeggings() : null, + shouldStayOnDeath(inventory.getBoots()) ? inventory.getBoots() : null, + shouldStayOnDeath(inventory.getItemInOffHand()) ? inventory.getItemInOffHand() : null, + hotbarItems, inventoryItems + )).right; + } + + public static CrateData readCrate(String ulid) { + return crateStorage.getData(ulid); + } + + public static void deleteCrate(String ulid) { + crateStorage.deleteData(ulid); + } + + public static ItemStack makeKey(String ulid, Location location) { + ItemStack item = new ItemStack(Material.TRIPWIRE_HOOK, 1); + ItemMeta meta = item.getItemMeta(); + meta.displayName(Component.text("Death Crate Key").color(NamedTextColor.DARK_RED).decoration(TextDecoration.ITALIC, false)); + meta.lore(List.of( + Component.text("Location: %s, %s, %s in %s".formatted(location.getBlockX(), location.getBlockY(), location.getBlockZ(), + location.getWorld().getName())).color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true), + Component.text("ULID: %s".formatted(ulid)).color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true), + Component.empty(), + Component.text("Unlocks the crate at the location above. Can be used by anyone.").color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true) + )); + meta.getPersistentDataContainer().set(KEY, PersistentDataType.STRING, ulid); + item.setItemMeta(meta); + return item; + } + + public static String getKeyULID(ItemStack item) { + if (item == null) { return null; } + if (!item.hasItemMeta()) { return null; } + return item.getItemMeta().getPersistentDataContainer().get(KEY, PersistentDataType.STRING); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/CompressionProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/CompressionProvider.java new file mode 100644 index 0000000..27162fd --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/CompressionProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data; + +public abstract class CompressionProvider { + public abstract String fileExtension(); + public abstract byte[] compress(byte[] input); + public abstract byte[] decompress(byte[] input); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/DataStorage.java b/src/main/java/de/blazemcworld/blazinggames/data/DataStorage.java new file mode 100644 index 0000000..09d70a7 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/DataStorage.java @@ -0,0 +1,350 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.utils.Pair; + +public class DataStorage { + public static final String DATA_STORAGE_DIRECTORY = "blazinggames"; + public static final String INDEX_FILE_NAME = "__datastorage_index.json"; + private static final File rootDir = new File(DATA_STORAGE_DIRECTORY); + static { + if (!rootDir.exists()) { + rootDir.mkdirs(); + } + } + + /** + * Creates a new DataStorage for the given class + * @param The type of data being stored + * @param The type of the name identifiers + * @param clazz The class storing the data (use getClass()) + * @param tableId Unique table identifier (null if you don't want to use tables, useful for storing many data types) + * @param storage Storage provider + * @param name Naming provider + * @param compression Compression provider + * @return DataStorage + */ + public static DataStorage forClass(Class clazz, String tableId, StorageProvider storage, NameProvider name, CompressionProvider compression) { + String dirName = tableId == null ? clazz.getName() : clazz.getName() + "+" + tableId; + File dir = new File(rootDir, dirName); + + if (!dir.exists()) { + dir.mkdirs(); + } + + return new DataStorage<>(dir, storage, name, compression); + } + + // /** + // * Creates a new DataStorage for the given class, with an indexer + // * @param The type of data being stored + // * @param The type of the name identifiers + // * @param clazz The class storing the data (use getClass()) + // * @param storage Storage provider + // * @param name Naming provider + // * @param compression Compression provider + // * @param indexer Indexer + // * @return DataStorage + // */ + // public static DataStorage forClass(Class clazz, StorageProvider storage, NameProvider name, CompressionProvider compression, Consumer> indexer) { + // String className = clazz.getName(); + // File dir = new File(rootDir, className); + + // if (!dir.exists()) { + // dir.mkdirs(); + // } + + // return new DataStorage<>(dir, storage, name, compression); + // } + + public final File dir; + public final StorageProvider storage; + public final NameProvider name; + public final CompressionProvider compression; + public DataStorage(File dir, StorageProvider storage, NameProvider name, CompressionProvider compression) { + this.dir = dir; + this.storage = storage; + this.name = name; + this.compression = compression; + // this.indexer = null; + // this.index = null; + } + + // public final Consumer> indexer; + // public final HashMap index; + // public DataStorage(File dir, StorageProvider storage, NameProvider name, CompressionProvider compression, Consumer> indexer) { + // this.dir = dir; + // this.storage = storage; + // this.name = name; + // this.compression = compression; + // this.indexer = indexer; + // this.index = new HashMap<>(); + // } + + /** + * Utility to read an entire file as a byte[] + */ + private byte[] readFile(File file) { + if (!file.exists()) { + return null; + } + + try (var fis = new FileInputStream(file)) { + return fis.readAllBytes(); + } catch (IOException e) { + BlazingGames.get().log(e); + return null; + } + } + + /** + * Utility to write a byte[] to a file + */ + private void writeFile(File file, byte[] data) { + try { + if (!file.exists()) { + file.createNewFile(); + } + } catch (IOException e) { + BlazingGames.get().log(e); + return; + } + + try (var fos = new FileOutputStream(file)) { + fos.write(data); + } catch (IOException e) { + BlazingGames.get().log(e); + } + } + + /** + * Synchronized file usage + */ + private void useFile(I identifier, Consumer callback) { + useFile(identifier, file -> { + callback.accept(file); + return null; + }); + } + + /** + * Synchronized file usage (with return value) + */ + private synchronized V useFile(I identifier, Function callback) { + StringBuilder out = new StringBuilder(name.fromValue(identifier)); + + if (storage.fileExtension() != null) { + out.append(".").append(storage.fileExtension()); + } + + if (compression.fileExtension() != null) { + out.append(".").append(compression.fileExtension()); + } + + File file = new File(dir, out.toString()); + return callback.apply(file); + } + + private String stripFileExtension(String filename) { + int toRemove = 0; + + if (compression.fileExtension() != null) { + toRemove += 1; + } + + if (storage.fileExtension() != null) { + toRemove += 1; + } + + List parts = new ArrayList<>(List.of(filename.split("\\."))); + + // remove the last toRemove parts + while (toRemove > 0) { + parts.remove(parts.size() - 1); + toRemove--; + } + + return String.join(".", parts); + } + + /** + * Gets the data currently stored for the given identifier + * @param identifier The identifier + * @return Data, or null if it can't be found + */ + public T getData(I identifier) { + return getData(identifier, null); + } + + /** + * Gets the data currently stored for this identifier + * @param identifier The identifier + * @param defaultValue Default value to return if the data can't be found + * @return Data, or defaultValue if it can't be found + */ + public T getData(I identifier, T defaultValue) { + return useFile(identifier, file -> { + if (!file.exists()) return null; + byte[] contents = readFile(file); + if (contents == null) return null; + byte[] decompressed = compression.decompress(contents); + return storage.read(decompressed); + }); + } + + /** + * Construct a new instance of a class with the given identifier, and store it + * @param applyId Function to create a new instance of T with the I specified + * @return The identifier used and data constructed + */ + public Pair storeNext(Function constructor) { + I id = name.next(); + if (id == null) { + throw new UnsupportedOperationException("Your name provider doesn't support next() values"); + } + T data = constructor.apply(id); + storeData(id, data); + return new Pair<>(data, id); + } + + /** + * Stores data and returns an identifier for it + * @param data Data to store + * @return New identifier to find the same data + */ + public I storeNext(T data) { + I id = name.next(); + if (id == null) { + throw new UnsupportedOperationException("Your name provider doesn't support next() values"); + } + storeData(id, data); + return id; + } + + /** + * Stores data with an identifier + * @param identifier The identifier + * @param data The data to store + */ + public void storeData(I identifier, T data) { + useFile(identifier, file -> { + byte[] bytes = storage.write(data); + byte[] compressed = compression.compress(bytes); + writeFile(file, compressed); + }); + } + + /** + * Deletes stored data + * @param identifier The identifier of the data to delete + */ + public void deleteData(I identifier) { + useFile(identifier, file -> { + if (file.exists()) { + file.delete(); + } + }); + } + + // /** + // * Get data from the index + // * @param key Index key + // * @return Data, if it exists + // */ + // @SuppressWarnings("unchecked") + // public V queryIndex(String key, Class clazz) { + // if (index == null) return null; + // if (!index.containsKey(key)) return null; + // if (clazz.isAssignableFrom(index.get(key).getClass())) return (V) index.get(key); + // return null; + // } + + /** + * Get all identifiers of data where the predicate matches + * @param predicate Predicate to test with + * @return List of matching data identifiers + */ + public List query(Predicate predicate) { + try (Stream paths = Files.walk(dir.toPath())) { + return paths.filter(Files::isRegularFile).map(path -> name.fromString(stripFileExtension(path.toFile().getName()))).filter(i -> { + T data = getData(i, null); + if (data == null) return false; + return predicate.test(data); + }).collect(Collectors.toList()); + } catch (IOException e) { + BlazingGames.get().log(e); + return List.of(); + } + } + + /** + * Get data where the predicate matches + * @param predicate Predicate to test with + * @return List of matching data + */ + public List queryForData(Predicate predicate) { + return query(predicate).stream().map(this::getData).collect(Collectors.toList()); + } + + /** + * Get all identifiers for data matching a certain condition + * @param predicate Predicate to test with + * @return List of matching data identifiers + */ + public List queryIdentifiers(Predicate predicate) { + try (Stream paths = Files.walk(dir.toPath())) { + return paths.filter(Files::isRegularFile).map(path -> name.fromString(stripFileExtension(path.toFile().getName()))) + .filter(predicate::test).collect(Collectors.toList()); + } catch (IOException e) { + BlazingGames.get().log(e); + return List.of(); + } + } + + /** + * Get all data where the identifiers match a certain condition + * @param predicate Predicate to test with + * @return List of matching data + */ + public List queryIdentifiersForData(Predicate predicate) { + return queryIdentifiers(predicate).stream().map(this::getData).collect(Collectors.toList()); + } + + /** + * Checks if data exists. Works even if the data stored is null + * @param identifier The identifier for the data + * @return If the stored data's file actually exists, without reading it + */ + public boolean hasData(I identifier) { + return useFile(identifier, File::exists); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/NameProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/NameProvider.java new file mode 100644 index 0000000..e6af804 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/NameProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data; + +public abstract class NameProvider { + public abstract T next(); + public abstract String fromValue(T value); + public abstract T fromString(String string); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/StorageProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/StorageProvider.java new file mode 100644 index 0000000..e1fd971 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/StorageProvider.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data; + +public abstract class StorageProvider { + public abstract String fileExtension(); + public abstract T read(byte[] data); + public abstract byte[] write(T data); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/compression/AntiCompressionProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/compression/AntiCompressionProvider.java new file mode 100644 index 0000000..698de28 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/compression/AntiCompressionProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.compression; + +import de.blazemcworld.blazinggames.data.CompressionProvider; + +public class AntiCompressionProvider extends CompressionProvider { + @Override + public String fileExtension() { + return null; + } + + @Override + public byte[] compress(byte[] input) { + return input; + } + + @Override + public byte[] decompress(byte[] input) { + return input; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/compression/GZipCompressionProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/compression/GZipCompressionProvider.java new file mode 100644 index 0000000..5d60c65 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/compression/GZipCompressionProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.compression; + +import de.blazemcworld.blazinggames.data.CompressionProvider; +import de.blazemcworld.blazinggames.utils.GZipToolkit; + +public class GZipCompressionProvider extends CompressionProvider { + @Override + public String fileExtension() { + return "gz"; + } + + @Override + public byte[] compress(byte[] input) { + return GZipToolkit.compressBytes(input); + } + + @Override + public byte[] decompress(byte[] input) { + return GZipToolkit.decompressBytes(input); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/providers/ArbitraryNameProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/providers/ArbitraryNameProvider.java new file mode 100644 index 0000000..a6e21ea --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/providers/ArbitraryNameProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.providers; + +import java.util.function.Supplier; + +import de.blazemcworld.blazinggames.data.NameProvider; + +public class ArbitraryNameProvider extends NameProvider { + protected final Supplier supplier; + + + public ArbitraryNameProvider() { + this.supplier = () -> null; + } + + public ArbitraryNameProvider(final String value) { + this.supplier = () -> value; + } + + public ArbitraryNameProvider(final Supplier supplier) { + this.supplier = supplier; + } + + @Override + public String next() { + return supplier.get(); + } + + @Override + public String fromValue(String value) { + return value; + } + + @Override + public String fromString(String string) { + return string; + } + +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/providers/ULIDNameProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/providers/ULIDNameProvider.java new file mode 100644 index 0000000..37c9014 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/providers/ULIDNameProvider.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.providers; + +import de.blazemcworld.blazinggames.data.NameProvider; +import io.azam.ulidj.MonotonicULID; + +public class ULIDNameProvider extends NameProvider { + protected final MonotonicULID ulid = new MonotonicULID(); + + @Override + public String next() { + return ulid.generate(); + } + + @Override + public String fromValue(String value) { + return value; + } + + @Override + public String fromString(String string) { + return string; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/providers/UUIDNameProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/providers/UUIDNameProvider.java new file mode 100644 index 0000000..316b62c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/providers/UUIDNameProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.providers; + +import java.util.UUID; + +import de.blazemcworld.blazinggames.data.NameProvider; + +public class UUIDNameProvider extends NameProvider { + @Override + public UUID next() { + return UUID.randomUUID(); + } + + @Override + public String fromValue(UUID value) { + return value.toString(); + } + + @Override + public UUID fromString(String string) { + return UUID.fromString(string); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/storage/BinaryStorageProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/storage/BinaryStorageProvider.java new file mode 100644 index 0000000..44a52f3 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/storage/BinaryStorageProvider.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.storage; + +import de.blazemcworld.blazinggames.data.StorageProvider; + +public class BinaryStorageProvider extends StorageProvider { + @Override + public String fileExtension() { + return "bin"; + } + + @Override + public byte[] read(byte[] data) { + return data; + } + + @Override + public byte[] write(byte[] data) { + return data; + } + +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/storage/GsonStorageProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/storage/GsonStorageProvider.java new file mode 100644 index 0000000..3d7d06e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/storage/GsonStorageProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.storage; + +import java.lang.reflect.Type; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.data.StorageProvider; + +public class GsonStorageProvider extends StorageProvider { + public GsonStorageProvider(Type type) { + this.type = type; + } + protected final Type type; + + @Override + public String fileExtension() { + return "json"; + } + + @Override + public T read(byte[] data) { + return BlazingGames.gson.fromJson(new String(data), type); + } + + @Override + public byte[] write(T data) { + return BlazingGames.gson.toJson(data, type).getBytes(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/data/storage/RawTextStorageProvider.java b/src/main/java/de/blazemcworld/blazinggames/data/storage/RawTextStorageProvider.java new file mode 100644 index 0000000..01c7ce7 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/data/storage/RawTextStorageProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.data.storage; + +import de.blazemcworld.blazinggames.data.StorageProvider; + +public class RawTextStorageProvider extends StorageProvider { + protected final String fileExtension; + public RawTextStorageProvider(String fileExtension) { + this.fileExtension = fileExtension; + } + + @Override + public String fileExtension() { + return fileExtension; + } + + @Override + public String read(byte[] data) { + return new String(data); + } + + @Override + public byte[] write(String data) { + return data.getBytes(); + } + +} diff --git a/src/main/java/de/blazemcworld/blazinggames/discord/AppConfig.java b/src/main/java/de/blazemcworld/blazinggames/discord/AppConfig.java new file mode 100644 index 0000000..b3c8839 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/discord/AppConfig.java @@ -0,0 +1,23 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.discord; + +public record AppConfig( + String token, + long channelId, + long consoleChannelId, + String webhookUrl +) { } diff --git a/src/main/java/de/blazemcworld/blazinggames/discord/DiscordApp.java b/src/main/java/de/blazemcworld/blazinggames/discord/DiscordApp.java new file mode 100644 index 0000000..3407472 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/discord/DiscordApp.java @@ -0,0 +1,366 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.discord; + +import club.minnced.discord.webhook.WebhookClient; +import club.minnced.discord.webhook.send.WebhookMessage; +import club.minnced.discord.webhook.send.WebhookMessageBuilder; +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.events.ChatEventListener; +import de.blazemcworld.blazinggames.utils.PlayerConfig; +import de.blazemcworld.blazinggames.utils.TextUtils; +import net.dv8tion.jda.api.JDA; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.channel.middleman.StandardGuildMessageChannel; +import net.dv8tion.jda.api.entities.sticker.Sticker; +import net.dv8tion.jda.api.entities.sticker.StickerItem; +import net.dv8tion.jda.api.events.message.MessageReceivedEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.BufferedInputStream; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.awt.Color; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.Random; +import java.util.function.Function; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.locks.ReentrantLock; + +public class DiscordApp extends ListenerAdapter { + /** + * Starts the bot. This operation blocks the thread. + */ + public static void init(AppConfig config) throws IllegalArgumentException { + if (app != null) return; + app = new DiscordApp(config); + } + + /** + * Stop the bot. This operation blocks the thread. + */ + public static void dispose() { + if (app == null) return; + app.stop(); + app = null; + } + + public static void messageHook(Player player, Component message) { + if (app == null) return; + app.sendDiscordMessage(player, TextUtils.stripColorCodes(TextUtils.componentToString(message))); + } + + /** + * Sends a notification to the channel. + * @param notification The notification to send + */ + public static void send(DiscordNotification notification) { + if (app == null) return; // might be disabled or failed to start + app.notify(notification); + } + + private static DiscordApp app = null; + private final JDA jda; + private final StandardGuildMessageChannel channel; + private final StandardGuildMessageChannel consoleChannel; + private long lastMessageId = 0; + private final WebhookClient client; + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + private final ReentrantLock lock = new ReentrantLock(); + private DiscordApp(AppConfig config) { + if (config.token() == null) { + throw new IllegalArgumentException("app token is not defined"); + } else if (config.webhookUrl() == null) { + throw new IllegalArgumentException("app webhook is not defined"); + } + + jda = JDABuilder + .createDefault(config.token()) + .addEventListeners(this) + .enableIntents(GatewayIntent.MESSAGE_CONTENT) + .build(); + try { + // block thread until JDA has started + jda.awaitReady(); + + this.channel = jda.getTextChannelById(config.channelId()); + if (this.channel == null) { + stop(); + throw new IllegalArgumentException("channelId is not a valid channel"); + } + this.consoleChannel = jda.getTextChannelById(config.consoleChannelId()); + if (this.consoleChannel == null) { + stop(); + throw new IllegalArgumentException("consoleChannelId is not a valid channel"); + } + + BufferedInputStream logs = null; + try { + logs = new BufferedInputStream(new FileInputStream("logs/latest.log")); + BufferedInputStream finalLogs = logs; + Bukkit.getScheduler().scheduleSyncRepeatingTask(BlazingGames.get(), () -> { + try { + String s = new String(finalLogs.readAllBytes()); + if (!s.isEmpty()) sendLogMessage(s); + } catch (IOException e) { + BlazingGames.get().log(e); + } + }, 0, 20); + } catch (FileNotFoundException ignored) {} + this.client = WebhookClient.withUrl(config.webhookUrl()); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void stop() { + jda.shutdown(); + try { + // block thread until JDA has stopped + jda.awaitShutdown(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onMessageReceived(@NotNull MessageReceivedEvent event) { + if (!event.isFromGuild()) return; + if (event.getAuthor().isBot()) return; + if (event.getChannel().getId().equals(this.channel.getId())) { + sendMinecraftMessage(Objects.requireNonNull(event.getMember()), + event.getMessage().getContentRaw(), + event.getMessage().getAttachments().toArray(new Message.Attachment[0]), + event.getMessage().getStickers().toArray(new StickerItem[0])); + } else if (event.getChannel().getId().equals(this.consoleChannel.getId())) { + lastMessageId = 0; + String command = event.getMessage().getContentRaw(); + if (command.startsWith("/")) command = command.substring(1); + String finalCommand = command; + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> Bukkit.getServer().dispatchCommand(Bukkit.getConsoleSender(), finalCommand)); + } + } + + private void sendDiscordMessage(Player player, String content) { + PlayerConfig config = PlayerConfig.forPlayer(player.getUniqueId()); + StringBuilder username = new StringBuilder(); + if (config.getDisplayName() != null && !config.getDisplayName().equals(player.getName())) { + username.append(config.getDisplayName()).append(" [aka ").append(player.getName()).append("]"); + } else { + username.append(player.getName()); + } + if (config.getPronouns() != null) { + username.append(" (").append(config.getPronouns()).append(")"); + } + if (player.isOp()) { + username.append(" \u266E"); + } + + String out; + if (ChatEventListener.meFormat(content) != null) { + out = ((config.getDisplayName() != null) ? config.getDisplayName() : player.getName()) + " " + ChatEventListener.meFormat(content); + } else if (ChatEventListener.greentextFormat(content) != null) { + StringBuilder builder = new StringBuilder(); + String[] parts = ChatEventListener.greentextFormat(content); + for (String part : parts) { + builder.append("\\> ").append(part).append("\n"); + } + out = builder.toString().trim(); + } else { + out = content; + } + + WebhookMessage message = new WebhookMessageBuilder() + .setUsername(username.toString()) + .setAvatarUrl("https://cravatar.eu/helmavatar/" + player.getUniqueId() + "/128.png") + .setContent(out) + .build(); + this.client.send(message); + } + + private void sendLogMessage(String content) { + executor.submit(() -> { + try { + lock.lock(); + if (lastMessageId == 0) { + Message last = consoleChannel.sendMessage("```\n\n```").complete(); + lastMessageId = last.getIdLong(); + } + Message last = consoleChannel.retrieveMessageById(lastMessageId).complete(); + String[] lines = content.split("\n"); + StringBuilder msgContent = new StringBuilder(); + int index = 0; + boolean editedLast = false; + for (String line : lines) { + index++; + if (!editedLast) { + if (last.getContentRaw().length() + msgContent.length() + line.length() + 5 < 2000) { + msgContent.append(line).append("\n"); + if (index == lines.length) { + String oldContent = last.getContentRaw().substring(0, last.getContentRaw().length() - 4); + last.editMessage(oldContent + msgContent + "\n```").complete(); + } + } else { + if (last.getContentRaw().length() + msgContent.length() + line.length() + 5 >= 2000 && index == lines.length) { + if (msgContent.isEmpty()) msgContent = new StringBuilder("```\n"); + if (!msgContent.toString().startsWith("```\n")) msgContent = new StringBuilder("```\n" + msgContent); + msgContent.append(line).append("\n"); + Message msg = consoleChannel.sendMessage(msgContent + "```").complete(); + lastMessageId = msg.getIdLong(); + } else { + if (msgContent.isEmpty()) msgContent = new StringBuilder("```\n"); + String oldContent = last.getContentRaw().substring(0, last.getContentRaw().length() - 4); + last.editMessage(oldContent + msgContent + "\n```").complete(); + msgContent = new StringBuilder("```\n" + line); + editedLast = true; + } + } + } else { + if (msgContent.length() + line.length() + 5 < 2000 && index != lines.length) { + msgContent.append(line).append("\n"); + } else { + if (msgContent.isEmpty()) msgContent = new StringBuilder("```\n"); + Message msg = consoleChannel.sendMessage(msgContent + "```").complete(); + lastMessageId = msg.getIdLong(); + msgContent = new StringBuilder("```\n" + line); + } + } + } + } finally { + lock.unlock(); + } + }); + } + + // https://stackoverflow.com/a/4247219 + private Component prettyName(String text) { + Random random = new Random(text.hashCode()); // use hashCode for consistency + final float hue = random.nextFloat(); + // Saturation between 0.1 and 0.3 + final float saturation = (random.nextInt(2000) + 1000) / 10000f; + final float luminance = 0.9f; + final Color color = Color.getHSBColor(hue, saturation, luminance); + return Component.text(text).color(TextColor.color(color.getRGB())); + } + + private Component formatArrayIntoComponent( + String title, T[] items, Function chatText, + Function hoverText, Function clickUrl + ) { + Component lBracket = Component.text("[").color(NamedTextColor.GRAY); + Component rBracket = Component.text("]").color(NamedTextColor.GRAY); + Component attachmentList = Arrays.stream(items).map(t -> lBracket.append(chatText.apply(t)).append(rBracket) + .hoverEvent(HoverEvent.showText(hoverText.apply(t))) + .clickEvent(ClickEvent.openUrl(clickUrl.apply(t))) + .color(NamedTextColor.GRAY)) + .reduce(Component.empty(), (textComponent, textComponent2) -> + Component.empty().equals(textComponent) ? textComponent.append(textComponent2) : + textComponent.append(Component.text(", ").color(NamedTextColor.WHITE)) + .append(textComponent2) + ); + return Component.newline().append(Component.text("↳ " + title + ": ").color(NamedTextColor.WHITE)).append(attachmentList); + } + + private static Component createAttachmentDataString(Message.Attachment attachment) { + Component description = (attachment.getDescription() == null) ? + Component.text("No description (alt text) provided").decorate(TextDecoration.ITALIC) : + Component.text(attachment.getDescription()); + Component metadata = Component.text(attachment.getContentType() + " - " + humanReadableByteCountBin(attachment.getSize())); + Component spoiler = attachment.isSpoiler() ? Component.newline().append(Component.text( + "This file is a spoiler!" + ).color(NamedTextColor.RED)) : Component.empty(); + return Component.empty().append(description).appendNewline().append(metadata).append(spoiler).appendNewline().appendNewline().append( + Component.text("Click to preview in browser") + ); + } + + // https://stackoverflow.com/a/3758880 + // changed to use Int instead of Long + public static String humanReadableByteCountBin(int bytes) { + long absB = bytes == Integer.MIN_VALUE ? Integer.MAX_VALUE : Math.abs(bytes); + if (absB < 1024) { + return bytes + " B"; + } + long value = absB; + CharacterIterator ci = new StringCharacterIterator("KMGTPE"); + for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) { + value >>= 10; + ci.next(); + } + value *= Long.signum(bytes); + return String.format("%.1f %ciB", value / 1024.0, ci.current()); + } + + private void sendMinecraftMessage(Member member, String content, Message.Attachment[] attachmentsRaw, Sticker[] stickersRaw) { + Component attachments = (attachmentsRaw.length > 0) ? formatArrayIntoComponent( + "Attachments", attachmentsRaw, attachment -> prettyName(attachment.getFileName()), + DiscordApp::createAttachmentDataString, // fun fact: if that method is not static, intellij complains for some reason + Message.Attachment::getUrl + ) : Component.empty(); + + Component stickers = (stickersRaw.length > 0) ? formatArrayIntoComponent( + "Stickers", stickersRaw, sticker -> prettyName(sticker.getName()), + sticker -> Component.text("Open Discord to view this sticker"), + sticker -> "" + ) : Component.empty(); + + Component messageSegment; + if (!content.isBlank()) { + messageSegment = Component.text(": ") + .color(NamedTextColor.WHITE) + .append(TextUtils.colorCodeParser(TextUtils.stringToComponent(content).color(NamedTextColor.WHITE))); + } else if (attachmentsRaw.length > 0) { + messageSegment = Component.text(" sent attachments").color(NamedTextColor.WHITE); + } else if (stickersRaw.length > 0) { + messageSegment = Component.text(" sent stickers").color(NamedTextColor.WHITE); + } else { + messageSegment = Component.text(" sent something").color(NamedTextColor.WHITE); + } + + Bukkit.broadcast(Component.text() + .append(Component.text("[DISCORD] ") + .color(TextColor.color(0x2d4386))) + .append(Component.text(member.getEffectiveName()) + .color(TextColor.color(member.getColorRaw()))) + .append(messageSegment) + .append(attachments) + .append(stickers) + .build()); + } + + private void notify(DiscordNotification notification) { + channel.sendMessageEmbeds(List.of(notification.toEmbed())) + .setSuppressedNotifications(true).queue(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/discord/DiscordNotification.java b/src/main/java/de/blazemcworld/blazinggames/discord/DiscordNotification.java new file mode 100644 index 0000000..6d43f5e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/discord/DiscordNotification.java @@ -0,0 +1,101 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.discord; + +import de.blazemcworld.blazinggames.utils.TextUtils; +import io.papermc.paper.advancement.AdvancementDisplay; +import net.dv8tion.jda.api.EmbedBuilder; +import net.dv8tion.jda.api.entities.MessageEmbed; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.awt.Color; + +public record DiscordNotification( + Player player, + String title, + String description, + Color color, + String iconUrl +) { + public static DiscordNotification serverStartup() { + return new DiscordNotification( + null, "Server Started", null, Color.GREEN, null + ); + } + + public static DiscordNotification serverShutdown() { + return new DiscordNotification( + null, "Server Stopped", null, Color.RED, null + ); + } + + public static DiscordNotification playerJoin(Player player) { + int players = Bukkit.getOnlinePlayers().size(); + String verbText = players == 1 ? "is" : "are"; + String playersText = players == 1 ? "player" : "players"; + return new DiscordNotification( + player, "Joined the game", + "There " + verbText + " now " + players + " " + playersText + " online", + Color.GREEN, null + ); + } + + public static DiscordNotification playerLeave(Player player) { + int players = Bukkit.getOnlinePlayers().size() - 1; + String verbText = players == 1 ? "is" : "are"; + String playersText = players == 1 ? "player" : "players"; + return new DiscordNotification( + player, "Left the game", + "There " + verbText + " now " + players + " " + playersText + " online", + Color.RED, null + ); + } + + public static DiscordNotification playerAdvancement(Player player, AdvancementDisplay advancement) { + return new DiscordNotification( + player, "Obtained " + TextUtils.componentToString(advancement.title()), + TextUtils.componentToString(advancement.description()), Color.YELLOW, + "https://raw.githubusercontent.com/Owen1212055/mc-assets/main/assets/" + + advancement.icon().getType() + ".png" + ); + } + + public static DiscordNotification playerDeath(Player player, String deathMessage) { + return new DiscordNotification( + player, "Died", + deathMessage, Color.ORANGE, null + ); + } + + public MessageEmbed toEmbed() { + EmbedBuilder builder = new EmbedBuilder(); + + // defaults + builder.setTitle("Notification"); + builder.setColor(Color.ORANGE); + + if (player != null) builder.setAuthor( + player.getName(), null, + "http://cravatar.eu/helmhead/" + player.getUniqueId() + "/128.png" + ); + if (title != null) builder.setTitle(title); + if (description != null) builder.setDescription(description); + if (color != null) builder.setColor(color); + if (iconUrl != null) builder.setThumbnail(iconUrl); + return builder.build(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/BaneOfIllagersEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/BaneOfIllagersEnchantment.java new file mode 100644 index 0000000..2e1c48a --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/BaneOfIllagersEnchantment.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Illager; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class BaneOfIllagersEnchantment extends CustomEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("bane_of_illagers"); + } + + @Override + public @NotNull String getDisplayName() { + return "Bane of Illagers"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.WEAPON; + } + + public int getMaxLevel() { + return 5; + } + + public double getDamageIncrease(Entity victim, int level) { + if(victim instanceof Illager) { + return level*2.5; + } + return 0; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + return altarTier + 1; + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.IRON_AXE); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, level, switch(level) { + case 3, 4 -> 16; + default -> 1; + }, switch(level) { + case 2 -> new MaterialItemPredicate(Material.IRON_AXE); + case 3 -> new MaterialItemPredicate(Material.EMERALD); + case 4 -> new MaterialItemPredicate(Material.EMERALD_BLOCK); + case 5 -> new MaterialItemPredicate(Material.TOTEM_OF_UNDYING); + default -> new MaterialItemPredicate(Material.SADDLE); + } + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/CapturingEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/CapturingEnchantment.java new file mode 100644 index 0000000..50b67b4 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/CapturingEnchantment.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomTreasureEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class CapturingEnchantment extends CustomTreasureEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("capturing"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.CREEPER_SPAWN_EGG); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, level*4, 16, switch(level) { + case 2 -> new MaterialItemPredicate(Material.SPIDER_EYE); + case 3 -> new MaterialItemPredicate(Material.EGG); + default -> new MaterialItemPredicate(Material.GUNPOWDER); + } + ); + } + + @Override + public @NotNull String getDisplayName() { + return "Capturing"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.WEAPON; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier <= 2) return 0; + if (altarTier == 4) return 3; + return altarTier - 1; + } + + public int getMaxLevel() { + return 3; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/CollectableEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/CollectableEnchantment.java new file mode 100644 index 0000000..38e77af --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/CollectableEnchantment.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomSingleLeveledEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class CollectableEnchantment extends CustomSingleLeveledEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("collectable"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.HOPPER_MINECART); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe(1, 2, 4, new MaterialItemPredicate(Material.ENDER_PEARL)); + } + + @Override + public @NotNull String getDisplayName() { + return "Collectable"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.TOOL; + } + + @Override + protected boolean allowAltarTier(int altarTier) { + return altarTier >= 2; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/FlameTouchEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/FlameTouchEnchantment.java new file mode 100644 index 0000000..218e121 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/FlameTouchEnchantment.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentType; +import de.blazemcworld.blazinggames.enchantments.sys.CustomSingleLeveledEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class FlameTouchEnchantment extends CustomSingleLeveledEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("flame_touch"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.BLAZE_POWDER); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.BLAZE_POWDER)); + } + + @Override + public @NotNull String getDisplayName() { + return "Flame Touch"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.TOOL; + } + + public @NotNull CustomEnchantmentType getEnchantmentType() { + return CustomEnchantmentType.TWISTED; + } + + @Override + protected boolean allowAltarTier(int altarTier) { + return altarTier >= 3; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/HellInfusionEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/HellInfusionEnchantment.java new file mode 100644 index 0000000..d8eaf0d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/HellInfusionEnchantment.java @@ -0,0 +1,91 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.World; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class HellInfusionEnchantment extends CustomEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("hell_infusion"); + } + + @Override + public @NotNull String getDisplayName() { + return "Hell Infusion"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.WEAPON; + } + + public int getMaxLevel() { + return 3; + } + + public double getDamageIncrease(Entity victim, int level) { + if(victim.getWorld().getEnvironment() == World.Environment.NETHER) { + return level*level*0.5; + } + return 0; + } + + public boolean conflictsWith(Enchantment enchantment) { + return enchantment == Enchantment.SHARPNESS + || enchantment == Enchantment.SMITE + || enchantment == Enchantment.BANE_OF_ARTHROPODS; + } + + public boolean conflictsWith(@NotNull CustomEnchantment enchantment) { + return enchantment == CustomEnchantments.SEA_INFUSION + || enchantment == CustomEnchantments.BANE_OF_ILLAGERS; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier <= 1) return 0; + return altarTier - 1; + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.MAGMA_BLOCK); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, level * 4, level == 3 ? 64 : 32, switch(level) { + case 2 -> new MaterialItemPredicate(Material.MAGMA_BLOCK); + case 3 -> new MaterialItemPredicate(Material.BLAZE_POWDER); + default -> new MaterialItemPredicate(Material.MAGMA_CREAM); + } + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/NatureBlessingEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/NatureBlessingEnchantment.java new file mode 100644 index 0000000..a937b48 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/NatureBlessingEnchantment.java @@ -0,0 +1,60 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.BlazingEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomSingleLeveledEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class NatureBlessingEnchantment extends CustomSingleLeveledEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("nature_blessing"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.WHEAT); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + 1, 4, 32, new MaterialItemPredicate(Material.BONE_BLOCK) + ); + } + + @Override + public @NotNull String getDisplayName() { + return "Nature's Blessing"; + } + + public CustomEnchantmentTarget getItemTarget() { + return BlazingEnchantmentTarget.HOE; + } + + @Override + protected boolean allowAltarTier(int altarTier) { + return true; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/PatternEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/PatternEnchantment.java new file mode 100644 index 0000000..1f47747 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/PatternEnchantment.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import de.blazemcworld.blazinggames.utils.Triple; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class PatternEnchantment extends CustomEnchantment { + public static List> dimensions = List.of( + new Triple<>(1,2, Material.WOODEN_PICKAXE), + new Triple<>(2,2, Material.STONE_PICKAXE), + new Triple<>(3,3, Material.IRON_PICKAXE) + ); + + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("pattern"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.IRON_PICKAXE); + } + + @Override + public AltarRecipe getRecipe(int level) { + if(level <= 0) { + return getRecipe(1); + } + + if(level > dimensions.size()) { + return getRecipe(dimensions.size()); + } + + return new AltarRecipe(level, level * 4, new MaterialItemPredicate(dimensions.get(level-1).right)); + } + + @Override + public @NotNull String getDisplayName() { + return "Pattern"; + } + + public @NotNull String getDisplayLevel(int level) { + if(level <= 0 || level > dimensions.size()) { + return super.getDisplayLevel(level); + } + + return dimensions.get(level-1).left + "x" + dimensions.get(level-1).middle; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.TOOL; + } + + public int getMaxLevel() { + return 3; + } + + public boolean conflictsWith(@NotNull CustomEnchantment enchantment) { + return enchantment == CustomEnchantments.TREE_FELLER; + } + + @Override + public boolean canUpgradeLevel(int currentLevel) { + return false; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/ReflectiveDefensesEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/ReflectiveDefensesEnchantment.java new file mode 100644 index 0000000..de08950 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/ReflectiveDefensesEnchantment.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.BlazingEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class ReflectiveDefensesEnchantment extends CustomEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("reflective_defenses"); + } + + @Override + public @NotNull String getDisplayName() { + return "Reflective Defenses"; + } + + public CustomEnchantmentTarget getItemTarget() { + return BlazingEnchantmentTarget.SHIELD; + } + + public int getMaxLevel() { + return 5; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + return altarTier + 1; + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.SHIELD); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, + level * 2, + level == 5 ? 1 : 16, + switch(level) { + case 2 -> new MaterialItemPredicate(Material.SWEET_BERRIES); + case 3 -> new MaterialItemPredicate(Material.PRISMARINE_SHARD); + case 4 -> new MaterialItemPredicate(Material.PRISMARINE_CRYSTALS); + case 5 -> new MaterialItemPredicate(Material.DIAMOND_BLOCK); + default -> new MaterialItemPredicate(Material.CACTUS); + } + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/ScavengerEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/ScavengerEnchantment.java new file mode 100644 index 0000000..0af134f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/ScavengerEnchantment.java @@ -0,0 +1,64 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomTreasureEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class ScavengerEnchantment extends CustomTreasureEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("scavenger"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.GOLD_INGOT); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, level*8, + 64, switch(level) { + case 2 -> new MaterialItemPredicate(Material.DIAMOND); + case 3 -> new MaterialItemPredicate(Material.NETHERITE_SCRAP); + default -> new MaterialItemPredicate(Material.GOLD_INGOT); + } + ); + } + + @Override + public @NotNull String getDisplayName() { + return "Scavenger"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.WEAPON; + } + + public int getMaxLevel() { + return 3; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/SeaInfusionEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/SeaInfusionEnchantment.java new file mode 100644 index 0000000..2c17300 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/SeaInfusionEnchantment.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.BlazingEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class SeaInfusionEnchantment extends CustomEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("sea_infusion"); + } + + @Override + public @NotNull String getDisplayName() { + return "Sea Infusion"; + } + + public CustomEnchantmentTarget getItemTarget() { + return BlazingEnchantmentTarget.WEAPON_TRIDENT; + } + + public int getMaxLevel() { + return 3; + } + + public double getDamageIncrease(Entity victim, int level) { + if(victim.isInWaterOrRainOrBubbleColumn()) { + return level*level*0.5; + } + return 0; + } + + public boolean conflictsWith(Enchantment enchantment) { + return enchantment == Enchantment.SHARPNESS + || enchantment == Enchantment.SMITE + || enchantment == Enchantment.BANE_OF_ARTHROPODS; + } + + public boolean conflictsWith(@NotNull CustomEnchantment enchantment) { + return enchantment == CustomEnchantments.HELL_INFUSION + || enchantment == CustomEnchantments.BANE_OF_ILLAGERS; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier <= 1) return 0; + return altarTier - 1; + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.TUBE_CORAL_BLOCK); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, level * 4, level == 3 ? 32 : 16, switch(level) { + case 2 -> new MaterialItemPredicate(Material.TUBE_CORAL_BLOCK); + case 3 -> new MaterialItemPredicate(Material.SPONGE); + default -> new MaterialItemPredicate(Material.TUBE_CORAL); + } + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/TreeFellerEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/TreeFellerEnchantment.java new file mode 100644 index 0000000..9037c1c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/TreeFellerEnchantment.java @@ -0,0 +1,80 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.BlazingEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class TreeFellerEnchantment extends CustomEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("tree_feller"); + } + + @Override + public @NotNull String getDisplayName() { + return "Tree Feller"; + } + + public CustomEnchantmentTarget getItemTarget() { + return BlazingEnchantmentTarget.AXE; + } + + public int getMaxLevel() { + return 5; + } + + public boolean conflictsWith(@NotNull CustomEnchantment enchantment) { + return enchantment == CustomEnchantments.PATTERN; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier >= 4) return 5; + + return altarTier; + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.OAK_SAPLING); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, + level * 3, + 32, + switch(level) { + case 2 -> new MaterialItemPredicate(Material.JUNGLE_LOG); + case 3 -> new MaterialItemPredicate(Material.CHERRY_LOG); + case 4 -> new MaterialItemPredicate(Material.WARPED_STEM); + case 5 -> new MaterialItemPredicate(Material.CHORUS_FLOWER); + default -> new MaterialItemPredicate(Material.OAK_LOG); + } + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/UnshinyEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/UnshinyEnchantment.java new file mode 100644 index 0000000..4260ad3 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/UnshinyEnchantment.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentType; +import de.blazemcworld.blazinggames.enchantments.sys.CustomTreasureSingleLeveledEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.PaperEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class UnshinyEnchantment extends CustomTreasureSingleLeveledEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("unshiny"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.INK_SAC); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe(1, 0, new MaterialItemPredicate(Material.INK_SAC)); + } + + @Override + public @NotNull String getDisplayName() { + return "Unshiny"; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.ALL; + } + + public @NotNull CustomEnchantmentType getEnchantmentType() { + return CustomEnchantmentType.COSMETIC; + } + + @Override + protected boolean allowAltarTier(int altarTier) { + return true; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/UpdraftEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/UpdraftEnchantment.java new file mode 100644 index 0000000..ddc5661 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/UpdraftEnchantment.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.BlazingEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantmentTarget; +import de.blazemcworld.blazinggames.enchantments.sys.CustomTreasureEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public class UpdraftEnchantment extends CustomTreasureEnchantment { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("updraft"); + } + + @Override + public ItemStack getPreIcon() { + return new ItemStack(Material.CAMPFIRE); + } + + @Override + public AltarRecipe getRecipe(int level) { + return new AltarRecipe( + level, level * 8, 16, new MaterialItemPredicate( + level == 2 ? Material.SOUL_CAMPFIRE : Material.CAMPFIRE) + ); + } + + @Override + public @NotNull String getDisplayName() { + return "Updraft"; + } + + public CustomEnchantmentTarget getItemTarget() { + return BlazingEnchantmentTarget.ELYTRA; + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier <= 1) return 0; + return altarTier - 1; + } + + public int getMaxLevel() { + return 2; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/BlazingEnchantmentTarget.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/BlazingEnchantmentTarget.java new file mode 100644 index 0000000..29483b6 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/BlazingEnchantmentTarget.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import org.bukkit.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + +public enum BlazingEnchantmentTarget implements CustomEnchantmentTarget { + AXE(Material.WOODEN_AXE, Material.STONE_AXE, Material.IRON_AXE, Material.GOLDEN_AXE, Material.DIAMOND_AXE, Material.NETHERITE_AXE), + HOE(Material.WOODEN_HOE, Material.STONE_HOE, Material.IRON_HOE, Material.GOLDEN_HOE, Material.DIAMOND_HOE, Material.NETHERITE_HOE), + SHIELD(Material.SHIELD), ELYTRA(Material.ELYTRA), + WEAPON_TRIDENT(Material.WOODEN_SWORD, Material.STONE_SWORD, Material.IRON_SWORD, Material.GOLDEN_SWORD, + Material.DIAMOND_SWORD, Material.NETHERITE_SWORD, Material.TRIDENT); + + final Set allowed; + + BlazingEnchantmentTarget(Material... allowed) { + this.allowed = Set.of(allowed); + } + + public boolean includes(@NotNull Material item) { + return allowed.contains(item); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantment.java new file mode 100644 index 0000000..1e1bf9a --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantment.java @@ -0,0 +1,152 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.utils.NumberUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.Set; + +public abstract class CustomEnchantment implements EnchantmentWrapper { + public abstract @NotNull NamespacedKey getKey(); + + @Override + public ItemStack apply(ItemStack tool, int level) { + return EnchantmentHelper.setCustomEnchantment(tool, this, level); + } + + @Override + public int getLevel(ItemStack tool) { + return EnchantmentHelper.getCustomEnchantmentLevel(tool, this); + } + + @Override + public int getMaxLevel() { + return 1; + } + + public CustomEnchantmentTarget getItemTarget() { + return PaperEnchantmentTarget.BREAKABLE; + } + + public boolean canEnchantItem(@NotNull ItemStack itemStack) { + Map customEnchantmentLevels = EnchantmentHelper.getCustomEnchantments(itemStack); + + for(Map.Entry entry : customEnchantmentLevels.entrySet()) { + if(conflictsWith(entry.getKey()) || entry.getKey().conflictsWith(this)) { + return false; + } + } + + ItemMeta meta = itemStack.getItemMeta(); + + Map enchantmentLevels = Map.of(); + + if(meta != null) { + enchantmentLevels = itemStack.getItemMeta().getEnchants(); + } + + for(Map.Entry entry : enchantmentLevels.entrySet()) { + if(conflictsWith(entry.getKey())) { + return false; + } + } + + if(itemStack.getItemMeta() instanceof EnchantmentStorageMeta esm) { + Map storedEnchantmentLevels = esm.getStoredEnchants(); + + for(Map.Entry entry : storedEnchantmentLevels.entrySet()) { + if(conflictsWith(entry.getKey())) { + return false; + } + } + } + + return canGoOnItem(itemStack); + } + + @Override + public boolean canGoOnItem(ItemStack tool) { + return getItemTarget().includes(tool) || tool.getType() == Material.BOOK + || tool.getType() == Material.ENCHANTED_BOOK; + } + + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier == 4) { + return getMaxLevel(); + } + + return Math.min(getMaxLevel(), altarTier); + } + + public boolean canUpgradeLevel(int currentLevel) { + return true; + } + + public @NotNull CustomEnchantmentType getEnchantmentType() { + return CustomEnchantmentType.NORMAL; + } + + @Override + public final @NotNull Component getComponent(int level) { + String levelText = getDisplayLevel(level); + + if(levelText.isBlank()) levelText = ""; + else levelText = " " + levelText; + + return Component.text(getDisplayName() + levelText).color(getEnchantmentType().getColor()).decoration(TextDecoration.ITALIC, false); + } + + public @NotNull String getDisplayLevel(int level) { + return NumberUtils.getRomanNumber(level); + } + + public abstract @NotNull String getDisplayName(); + + public @NotNull Set getActiveSlots() { + return Set.of(EquipmentSlot.values()); + } + + @Override + public String toString() { + return getKey().toString(); + } + + public double getDamageIncrease(Entity victim, int level) { + return 0; + } + + @Override + public Component getLevelessComponent() { + return Component.text(getDisplayName()).color(getEnchantmentType().getColor()).decoration(TextDecoration.ITALIC, false); + } + + @Override + public boolean isTreasure() { + return false; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantmentTarget.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantmentTarget.java new file mode 100644 index 0000000..9a28fcd --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantmentTarget.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; + +public interface CustomEnchantmentTarget { + boolean includes(@NotNull Material item); + default boolean includes(@NotNull ItemStack item) { + return includes(item.getType()); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantmentType.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantmentType.java new file mode 100644 index 0000000..1e35f75 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantmentType.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; + +public enum CustomEnchantmentType { + NORMAL(NamedTextColor.AQUA, true), + TWISTED(NamedTextColor.DARK_PURPLE, true), + COSMETIC(NamedTextColor.DARK_GRAY, true), + CURSED(NamedTextColor.DARK_RED, false); + + private final TextColor color; + private final boolean canBeRemoved; + + CustomEnchantmentType(TextColor color, boolean canBeRemoved) { + this.color = color; + this.canBeRemoved = canBeRemoved; + } + + public TextColor getColor() { + return color; + } + + public boolean canBeRemoved() { + return canBeRemoved; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantments.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantments.java new file mode 100644 index 0000000..a43dd46 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomEnchantments.java @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.enchantments.*; +import org.bukkit.NamespacedKey; + +import javax.annotation.Nullable; +import java.util.Set; + +public class CustomEnchantments { + public static final CustomEnchantment COLLECTABLE = new CollectableEnchantment(); + public static final CustomEnchantment PATTERN = new PatternEnchantment(); + public static final CustomEnchantment TREE_FELLER = new TreeFellerEnchantment(); + public static final CustomEnchantment CAPTURING = new CapturingEnchantment(); + public static final CustomEnchantment NATURE_BLESSING = new NatureBlessingEnchantment(); + public static final CustomEnchantment REFLECTIVE_DEFENSES = new ReflectiveDefensesEnchantment(); + public static final CustomEnchantment BANE_OF_ILLAGERS = new BaneOfIllagersEnchantment(); + public static final CustomEnchantment SEA_INFUSION = new SeaInfusionEnchantment(); + public static final CustomEnchantment HELL_INFUSION = new HellInfusionEnchantment(); + public static final CustomEnchantment UPDRAFT = new UpdraftEnchantment(); + public static final CustomEnchantment FLAME_TOUCH = new FlameTouchEnchantment(); + public static final CustomEnchantment UNSHINY = new UnshinyEnchantment(); + public static final CustomEnchantment SCAVENGER = new ScavengerEnchantment(); + + public static Set list() { + return Set.of( + COLLECTABLE, + PATTERN, + TREE_FELLER, + CAPTURING, + NATURE_BLESSING, + REFLECTIVE_DEFENSES, + BANE_OF_ILLAGERS, + SEA_INFUSION, + HELL_INFUSION, + UPDRAFT, + FLAME_TOUCH, + UNSHINY, + SCAVENGER + ); + } + + public static @Nullable CustomEnchantment getByKey(NamespacedKey key) { + for(CustomEnchantment curr : list()) { + if(curr.getKey().equals(key)) { + return curr; + } + } + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomSingleLeveledEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomSingleLeveledEnchantment.java new file mode 100644 index 0000000..c5afa53 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomSingleLeveledEnchantment.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import org.jetbrains.annotations.NotNull; + +public abstract class CustomSingleLeveledEnchantment extends CustomEnchantment { + @Override + public int maxLevelAvailableInAltar(int altarTier) { + return allowAltarTier(altarTier) ? 1 : 0; + } + + protected abstract boolean allowAltarTier(int altarTier); + + @Override + public @NotNull String getDisplayLevel(int level) { + return ""; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomTreasureEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomTreasureEnchantment.java new file mode 100644 index 0000000..130c2e2 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomTreasureEnchantment.java @@ -0,0 +1,23 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +public abstract class CustomTreasureEnchantment extends CustomEnchantment { + @Override + public boolean isTreasure() { + return true; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomTreasureSingleLeveledEnchantment.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomTreasureSingleLeveledEnchantment.java new file mode 100644 index 0000000..743d2c8 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/CustomTreasureSingleLeveledEnchantment.java @@ -0,0 +1,23 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +public abstract class CustomTreasureSingleLeveledEnchantment extends CustomSingleLeveledEnchantment { + @Override + public boolean isTreasure() { + return true; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentHelper.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentHelper.java new file mode 100644 index 0000000..faea757 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentHelper.java @@ -0,0 +1,344 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.utils.Pair; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import java.util.*; + +public class EnchantmentHelper { + private static final NamespacedKey key = BlazingGames.get().key("custom_enchantments"); + + public static Map getCustomEnchantments(ItemStack stack) { + if(stack == null || !stack.hasItemMeta()) { + return new HashMap<>(); + } + + PersistentDataContainer enchantments = stack.getItemMeta().getPersistentDataContainer() + .get(key, PersistentDataType.TAG_CONTAINER); + + Map enchantmentLevels = new HashMap<>(); + + if(enchantments != null) { + CustomEnchantments.list().forEach((customEnchantment) -> { + if(enchantments.has(customEnchantment.getKey(), PersistentDataType.INTEGER)) { + enchantmentLevels.put(customEnchantment, enchantments.get(customEnchantment.getKey(), PersistentDataType.INTEGER)); + } + }); + } + + return enchantmentLevels; + } + + public static ItemStack setCustomEnchantment(ItemStack stack, CustomEnchantment enchantment, int level) { + if(level == 0) + { + return removeCustomEnchantment(stack, enchantment); + } + + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return result; + } + + ItemMeta meta = result.getItemMeta(); + + PersistentDataContainer container = meta.getPersistentDataContainer(); + + PersistentDataContainer enchantments; + + if(!container.has(key, PersistentDataType.TAG_CONTAINER)) { + enchantments = container.getAdapterContext().newPersistentDataContainer(); + } + else { + enchantments = stack.getItemMeta().getPersistentDataContainer() + .get(key, PersistentDataType.TAG_CONTAINER); + } + + assert enchantments != null; + enchantments.set(enchantment.getKey(), PersistentDataType.INTEGER, level); + container.set(key, PersistentDataType.TAG_CONTAINER, enchantments); + + result.setItemMeta(meta); + + return updateTool(result); + } + + public static ItemStack removeCustomEnchantment(ItemStack stack, CustomEnchantment enchantment) { + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return result; + } + + ItemMeta meta = result.getItemMeta(); + + PersistentDataContainer container = meta.getPersistentDataContainer(); + + PersistentDataContainer enchantments; + + if(!container.has(key, PersistentDataType.TAG_CONTAINER)) { + return result; + } + else { + enchantments = stack.getItemMeta().getPersistentDataContainer() + .get(key, PersistentDataType.TAG_CONTAINER); + } + + assert enchantments != null; + enchantments.remove(enchantment.getKey()); + + if(enchantments.isEmpty()) + { + container.remove(key); + } + else + { + container.set(key, PersistentDataType.TAG_CONTAINER, enchantments); + } + + result.setItemMeta(meta); + + return updateTool(result); + } + + public static int getCustomEnchantmentLevel(ItemStack stack, CustomEnchantment enchantment) { + return getCustomEnchantments(stack).getOrDefault(enchantment, Integer.valueOf(0)); + } + + public static boolean hasCustomEnchantment(ItemStack stack, CustomEnchantment enchantment) { + return getCustomEnchantmentLevel(stack, enchantment) != 0; + } + + private static ItemStack updateTool(ItemStack stack) { + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return result; + } + + List lore = new ArrayList<>(); + + getCustomEnchantments(stack).forEach((enchantment, level) -> lore.add(enchantment.getComponent(level))); + + ItemMeta meta = result.getItemMeta(); + + try { + meta.setEnchantmentGlintOverride(null); + + if(getCustomEnchantments(stack).isEmpty()) { + if(result.getType() == Material.ENCHANTED_BOOK && meta instanceof EnchantmentStorageMeta esm) + { + if(!esm.hasStoredEnchants()) { + result = result.withType(Material.BOOK); + meta = result.getItemMeta(); + } + } + } + else { + if(result.getType() != Material.BOOK && result.getType() != Material.ENCHANTED_BOOK) + { + if(!meta.hasEnchants()) { + meta.setEnchantmentGlintOverride(true); + } + } + else if(result.getType() == Material.BOOK) + { + result = result.withType(Material.ENCHANTED_BOOK); + meta = result.getItemMeta(); + } + + if(hasCustomEnchantment(stack, CustomEnchantments.UNSHINY)) { + meta.setEnchantmentGlintOverride(false); + } + } + } + catch(Exception err) { + BlazingGames.get().log(err); + } + + meta.lore(lore); + + result.setItemMeta(meta); + + return result; + } + + public static boolean canEnchantItem(ItemStack stack) { + if(CustomItem.isCustomItem(stack)) return false; + + return PaperEnchantmentTarget.ALL.includes(stack) || stack.getType() == Material.ENCHANTED_BOOK + || stack.getType() == Material.BOOK; + } + + public static ItemStack enchantTool(ItemStack stack, CustomEnchantment enchantment, int level) { + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return result; + } + + if(enchantment.canEnchantItem(result)) { + int current = getCustomEnchantmentLevel(result, enchantment); + + if(current == level && current > 0) { + if(enchantment.canUpgradeLevel(current)) { + level++; + } + } + if(current > level) { + level = current; + } + if(level > enchantment.getMaxLevel()) { + level = enchantment.getMaxLevel(); + } + if(level < 0) { + level = 0; + } + + return setCustomEnchantment(result, enchantment, level); + } + + return result; + } + + public static Pair getCustomEnchantmentEntryByIndex(ItemStack stack, int index) { + index--; + + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return null; + } + + Map enchantmentLevels = getCustomEnchantments(result); + + for(Map.Entry enchantment : enchantmentLevels.entrySet()) { + if(index == 0) { + return new Pair<>(enchantment.getKey(), enchantment.getValue()); + } + if(!enchantment.getKey().getEnchantmentType().canBeRemoved()) { + continue; + } + index--; + } + + return null; + } + + public static Pair getEnchantmentEntryByIndex(ItemStack stack, int index) { + index--; + + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return null; + } + + Map enchantmentLevels; + + if(stack.getItemMeta() instanceof EnchantmentStorageMeta meta) { + enchantmentLevels = meta.getStoredEnchants(); + } + else { + enchantmentLevels = result.getEnchantments(); + } + + for(Enchantment enchantment : EnchantmentOrder.order()) { + if(enchantment.isCursed()) { + continue; + } + if(!enchantmentLevels.containsKey(enchantment)) { + continue; + } + if(index == 0) { + return new Pair<>(enchantment, enchantmentLevels.get(enchantment)); + } + index--; + } + + return null; + } + + public static ItemStack enchantFromItem(ItemStack in, ItemStack enchantingItem) { + ItemStack result = in.clone(); + + if(!canEnchantItem(result)) { + return result; + } + + Map enchantmentLevels = getCustomEnchantments(enchantingItem); + + for(Map.Entry enchantment : enchantmentLevels.entrySet()) { + result = enchantTool(result, enchantment.getKey(), enchantment.getValue()); + } + + return updateTool(result); + } + + public static boolean hasCustomEnchantments(ItemStack stack) { + return !getCustomEnchantments(stack).isEmpty(); + } + + public static ItemStack removeCustomEnchantments(ItemStack stack) { + ItemStack result = stack.clone(); + + if(!canEnchantItem(result)) { + return result; + } + + Set enchantments = getCustomEnchantments(stack).keySet(); + + for(CustomEnchantment enchantment : enchantments) { + if(enchantment.getEnchantmentType().canBeRemoved()) { + result = removeCustomEnchantment(result, enchantment); + } + } + + return updateTool(result); + } + + // This version of the getCustomEnchantmentLevel function ignores enchanted books + public static int getActiveCustomEnchantmentLevel(ItemStack stack, CustomEnchantment enchantment) { + if(stack != null && stack.getType() == Material.ENCHANTED_BOOK) { + return 0; + } + return getCustomEnchantmentLevel(stack, enchantment); + } + + // This version of the hasCustomEnchantment function ignores enchanted books + public static boolean hasActiveCustomEnchantment(ItemStack stack, CustomEnchantment enchantment) { + return getActiveCustomEnchantmentLevel(stack, enchantment) > 0; + } + + public static Map getActiveCustomEnchantments(ItemStack stack) { + if(stack != null && stack.getType() == Material.ENCHANTED_BOOK) { + return new HashMap<>(); + } + return getCustomEnchantments(stack); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentOrder.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentOrder.java new file mode 100644 index 0000000..a180c32 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentOrder.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import org.bukkit.enchantments.Enchantment; + +import java.util.List; + +public class EnchantmentOrder { + public static List order() { + return List.of( + Enchantment.BINDING_CURSE, + Enchantment.VANISHING_CURSE, + Enchantment.RIPTIDE, + Enchantment.CHANNELING, + Enchantment.FROST_WALKER, + Enchantment.SHARPNESS, + Enchantment.SMITE, + Enchantment.BANE_OF_ARTHROPODS, + Enchantment.IMPALING, + Enchantment.POWER, + Enchantment.PIERCING, + Enchantment.SWEEPING_EDGE, + Enchantment.MULTISHOT, + Enchantment.FIRE_ASPECT, + Enchantment.FLAME, + Enchantment.KNOCKBACK, + Enchantment.PUNCH, + Enchantment.PROTECTION, + Enchantment.BLAST_PROTECTION, + Enchantment.FIRE_PROTECTION, + Enchantment.PROJECTILE_PROTECTION, + Enchantment.FEATHER_FALLING, + Enchantment.FORTUNE, + Enchantment.LOOTING, + Enchantment.SILK_TOUCH, + Enchantment.LUCK_OF_THE_SEA, + Enchantment.EFFICIENCY, + Enchantment.QUICK_CHARGE, + Enchantment.LURE, + Enchantment.RESPIRATION, + Enchantment.AQUA_AFFINITY, + Enchantment.SOUL_SPEED, + Enchantment.SWIFT_SNEAK, + Enchantment.DEPTH_STRIDER, + Enchantment.THORNS, + Enchantment.LOYALTY, + Enchantment.UNBREAKING, + Enchantment.INFINITY, + Enchantment.MENDING + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentTome.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentTome.java new file mode 100644 index 0000000..4ab2071 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentTome.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.items.CustomItem; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class EnchantmentTome extends CustomItem { + private final NamespacedKey tomeKey; + private final String tomeName; + private final EnchantmentWrapper wrapper; + + public EnchantmentTome(NamespacedKey tomeKey, String tomeName, EnchantmentWrapper wrapper) { + this.tomeKey = tomeKey; + this.tomeName = tomeName; + this.wrapper = wrapper; + } + + @Override + public @NotNull NamespacedKey getKey() { + return tomeKey; + } + + @Override + protected @NotNull ItemStack material() { + ItemStack stack = new ItemStack(Material.BOOK); + + ItemMeta meta = stack.getItemMeta(); + meta.setEnchantmentGlintOverride(true); + + meta.itemName(Component.text(tomeName).color(NamedTextColor.LIGHT_PURPLE)); + meta.lore(List.of(getComponent())); + + stack.setItemMeta(meta); + + return stack; + } + + protected Component getComponent() { + return getWrapper().getLevelessComponent(); + } + public EnchantmentWrapper getWrapper() { + return wrapper; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentWrapper.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentWrapper.java new file mode 100644 index 0000000..07c261c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentWrapper.java @@ -0,0 +1,127 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.userinterfaces.UserInterface; +import de.blazemcworld.blazinggames.utils.NamespacedKeyDataType; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.ArrayList; +import java.util.List; + +public interface EnchantmentWrapper { + ItemStack apply(ItemStack tool, int level); + int getLevel(ItemStack tool); + int getMaxLevel(); + boolean canEnchantItem(ItemStack tool); + boolean canGoOnItem(ItemStack tool); + + default boolean conflictsWith(EnchantmentWrapper wrapper) { + if(wrapper instanceof VanillaEnchantmentWrapper vanilla) { + return conflictsWith(vanilla.getEnchantment()); + } + if(wrapper instanceof CustomEnchantment custom) { + conflictsWith(custom); + } + return false; + } + + default boolean conflictsWith(CustomEnchantment enchantment) { + return false; + } + + default boolean conflictsWith(Enchantment enchantment) { + return false; + } + + NamespacedKey getKey(); + Component getComponent(int level); + Component getLevelessComponent(); + + int maxLevelAvailableInAltar(int altarTier); + + ItemStack getPreIcon(); + + default ItemStack getIcon(ItemStack tool, int lapisAmount, ItemStack material, int tier) { + ItemStack result = getPreIcon(); + + int level = getLevel(tool); + + List lore = new ArrayList<>(); + + if(level < getMaxLevel()) { + if(level >= maxLevelAvailableInAltar(tier)) { + lore.add(Component.text("Can't upgrade any more with this tier of altar!") + .color(NamedTextColor.RED).decoration(TextDecoration.ITALIC, false)); + } + else { + lore.add(Component.text(level > 1 ? "Cost to upgrade to " : "Cost to acquire ") + .append(getComponent(level + 1)) + .append(Component.text(":")) + .color(NamedTextColor.YELLOW).decoration(TextDecoration.ITALIC, false) + ); + + AltarRecipe recipe = getRecipe(level + 1); + + Component name = recipe.itemRequirement().getDescription(); + + lore.add(Component.text(recipe.expAmount() + " Experience Levels") + .color(NamedTextColor.YELLOW) + .decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text(recipe.lapisAmount() + "x Lapis Lazuli") + .color(lapisAmount >= recipe.lapisAmount() ? NamedTextColor.GREEN : NamedTextColor.RED) + .decoration(TextDecoration.ITALIC, false)); + assert name != null; + lore.add(Component.text(recipe.itemAmount() + "x ") + .append(name) + .color(recipe.matchMaterial(material) ? NamedTextColor.GREEN : NamedTextColor.RED) + .decoration(TextDecoration.ITALIC, false)); + } + } + else { + lore.add(Component.text(getMaxLevel() > 1 ? "Max Level Achieved!" : "Acquired!") + .color(NamedTextColor.GREEN).decoration(TextDecoration.ITALIC, false)); + } + + ItemMeta meta = result.getItemMeta(); + meta.getPersistentDataContainer().set(UserInterface.guiKey, NamespacedKeyDataType.instance, getKey()); + meta.setMaxStackSize(getMaxLevel()); + meta.setHideTooltip(false); + meta.setEnchantmentGlintOverride(level > 0); + meta.itemName(level > 0 ? getComponent(level) : getLevelessComponent()); + meta.lore(lore); + meta.addItemFlags(ItemFlag.HIDE_ADDITIONAL_TOOLTIP); + result.setItemMeta(meta); + + if(level > 0) { + result.setAmount(level); + } + + return result; + } + + boolean isTreasure(); + + AltarRecipe getRecipe(int level); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentWrappers.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentWrappers.java new file mode 100644 index 0000000..8ebcc49 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/EnchantmentWrappers.java @@ -0,0 +1,355 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import de.blazemcworld.blazinggames.items.ColorlessItemPredicate; +import de.blazemcworld.blazinggames.items.MaterialItemPredicate; +import de.blazemcworld.blazinggames.items.PotionItemPredicate; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.potion.PotionEffectType; + +import javax.annotation.Nullable; +import java.util.HashSet; +import java.util.Set; + +public class EnchantmentWrappers { + public static VanillaEnchantmentWrapper MULTISHOT = + new VanillaEnchantmentWrapper(Enchantment.MULTISHOT, () -> new ItemStack(Material.PRISMARINE_CRYSTALS), + 0, 1, + new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.ARROW)) + ); + public static VanillaEnchantmentWrapper PIERCING = + new VanillaEnchantmentWrapper(Enchantment.PIERCING, () -> new ItemStack(Material.SPECTRAL_ARROW), + 1, 2, 3, + new AltarRecipe(1, 2, 32, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(2, 4, 32, new MaterialItemPredicate(Material.BONE)), + new AltarRecipe(3, 6, 32, new MaterialItemPredicate(Material.SPIDER_EYE)), + new AltarRecipe(4, 8, 32, new MaterialItemPredicate(Material.GUNPOWDER)) + ); + public static VanillaEnchantmentWrapper SILK_TOUCH = + new VanillaEnchantmentWrapper(Enchantment.SILK_TOUCH, () -> new ItemStack(Material.STRING), + 0, 0, 1, + new AltarRecipe(1, 8, 32, new MaterialItemPredicate(Material.STRING)) + ); + public static VanillaEnchantmentWrapper VANISHING_CURSE = + new VanillaEnchantmentWrapper(Enchantment.VANISHING_CURSE, () -> new ItemStack(Material.GLASS), + 0, 1, + new AltarRecipe(1, 8, new PotionItemPredicate(PotionEffectType.INVISIBILITY)) + ); + public static VanillaEnchantmentWrapper FROST_WALKER = + new VanillaEnchantmentWrapper(Enchantment.FROST_WALKER, () -> new ItemStack(Material.BLUE_ICE), + 0, 1, 2, + new AltarRecipe(1, 2, 16, new MaterialItemPredicate(Material.BLUE_ICE)), + new AltarRecipe(2, 4, 16, new MaterialItemPredicate(Material.PRISMARINE_CRYSTALS)) + ); + public static VanillaEnchantmentWrapper FORTUNE = + new VanillaEnchantmentWrapper(Enchantment.FORTUNE, () -> new ItemStack(Material.EMERALD), + 0, 1, 2, + new AltarRecipe(1, 2, 16, new MaterialItemPredicate(Material.EMERALD)), + new AltarRecipe(2, 4, 16, new MaterialItemPredicate(Material.DIAMOND)), + new AltarRecipe(3, 8, new MaterialItemPredicate(Material.RABBIT_FOOT)) + ); + public static VanillaEnchantmentWrapper BINDING_CURSE = + new VanillaEnchantmentWrapper(Enchantment.BINDING_CURSE, () -> new ItemStack(Material.CHAIN), + 0, 1, + new AltarRecipe(1, 8, 16, new MaterialItemPredicate(Material.COBWEB)) + ); + public static VanillaEnchantmentWrapper SHARPNESS = + new VanillaEnchantmentWrapper(Enchantment.SHARPNESS, () -> new ItemStack(Material.DIAMOND_SWORD), + 1, 2, 3, + new AltarRecipe(1, 2, 32, new MaterialItemPredicate(Material.REDSTONE)), + new AltarRecipe(2, 4, 16, new MaterialItemPredicate(Material.REDSTONE_BLOCK)), + new AltarRecipe(3, 6, 16, new MaterialItemPredicate(Material.BLAZE_POWDER)), + new AltarRecipe(4, 8, 8, new MaterialItemPredicate(Material.DIAMOND)), + new AltarRecipe(5, 10, new PotionItemPredicate(PotionEffectType.STRENGTH)) + ); + public static VanillaEnchantmentWrapper SWEEPING_EDGE = + new VanillaEnchantmentWrapper(Enchantment.SWEEPING_EDGE, () -> new ItemStack(Material.FEATHER), + 1, 2, 3, + new AltarRecipe(1, 1, 32, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(2, 2, 32, new MaterialItemPredicate(Material.BONE)), + new AltarRecipe(3, 4, 32, new MaterialItemPredicate(Material.STRING)) + ); + public static VanillaEnchantmentWrapper RIPTIDE = + new VanillaEnchantmentWrapper(Enchantment.RIPTIDE, () -> new ItemStack(Material.NAUTILUS_SHELL), + 0, 0, 1, + new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.CHAIN)), + new AltarRecipe(2, 6, 16, new MaterialItemPredicate(Material.PISTON)), + new AltarRecipe(3, 8, 16, new MaterialItemPredicate(Material.TNT)) + ); + public static VanillaEnchantmentWrapper QUICK_CHARGE = + new VanillaEnchantmentWrapper(Enchantment.QUICK_CHARGE, () -> new ItemStack(Material.REDSTONE), + 0, 1, 2, + new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.LEATHER)), + new AltarRecipe(2, 6, 16, new MaterialItemPredicate(Material.STRING)), + new AltarRecipe(3, 8, new PotionItemPredicate(PotionEffectType.SPEED)) + ); + public static VanillaEnchantmentWrapper IMPALING = + new VanillaEnchantmentWrapper(Enchantment.IMPALING, () -> new ItemStack(Material.TRIDENT), + 1, 2, 3, + new AltarRecipe(1, 1, new MaterialItemPredicate(Material.WATER_BUCKET)), + new AltarRecipe(2, 2, 32, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(3, 3, 16, new MaterialItemPredicate(Material.COD)), + new AltarRecipe(4, 4, 16, new MaterialItemPredicate(Material.SALMON)), + new AltarRecipe(5, 5, 16, new MaterialItemPredicate(Material.PUFFERFISH)) + ); + public static VanillaEnchantmentWrapper FIRE_PROTECTION = + new VanillaEnchantmentWrapper(Enchantment.FIRE_PROTECTION, () -> new ItemStack(Material.MAGMA_CREAM), + 1, 2, 3, + new AltarRecipe(1, 1, 32, new MaterialItemPredicate(Material.COPPER_INGOT)), + new AltarRecipe(2, 2, 32, new MaterialItemPredicate(Material.IRON_INGOT)), + new AltarRecipe(3, 3, 16, new MaterialItemPredicate(Material.BLAZE_POWDER)), + new AltarRecipe(4, 4, 32, new MaterialItemPredicate(Material.BLAZE_POWDER)) + ); + public static VanillaEnchantmentWrapper MENDING = + new VanillaEnchantmentWrapper(Enchantment.MENDING, () -> new ItemStack(Material.SHULKER_SHELL), + 0, 0, 0, + new AltarRecipe(10, 15, new MaterialItemPredicate(Material.NETHER_STAR)) + ); + public static VanillaEnchantmentWrapper LOYALTY = + new VanillaEnchantmentWrapper(Enchantment.LOYALTY, () -> new ItemStack(Material.STICK), + 0, 1, 2, + new AltarRecipe(1, 2, 16, new MaterialItemPredicate(Material.BONE)), + new AltarRecipe(2, 3, 16, new MaterialItemPredicate(Material.COD)), + new AltarRecipe(3, 4, 16, new MaterialItemPredicate(Material.CHAIN)) + ); + public static VanillaEnchantmentWrapper LUCK_OF_THE_SEA = + new VanillaEnchantmentWrapper(Enchantment.LUCK_OF_THE_SEA, () -> new ItemStack(Material.HEART_OF_THE_SEA), + 1, 2, 3, + new AltarRecipe(1, 1, 16, new MaterialItemPredicate(Material.COD)), + new AltarRecipe(2, 2, 8, new MaterialItemPredicate(Material.CHEST)), + new AltarRecipe(3, 4, new MaterialItemPredicate(Material.HEART_OF_THE_SEA)) + ); + public static VanillaEnchantmentWrapper RESPIRATION = + new VanillaEnchantmentWrapper(Enchantment.RESPIRATION, () -> new ItemStack(Material.PUFFERFISH), + 1, 2, 3, + new AltarRecipe(1, 1, new MaterialItemPredicate(Material.WATER_BUCKET)), + new AltarRecipe(2, 2, new MaterialItemPredicate(Material.TURTLE_HELMET)), + new AltarRecipe(3, 4, new PotionItemPredicate(PotionEffectType.WATER_BREATHING)) + ); + public static VanillaEnchantmentWrapper FLAME = + new VanillaEnchantmentWrapper(Enchantment.FLAME, () -> new ItemStack(Material.BLAZE_POWDER), + 0, 1, + new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.BLAZE_POWDER)) + ); + public static VanillaEnchantmentWrapper PUNCH = + new VanillaEnchantmentWrapper(Enchantment.PUNCH, () -> new ItemStack(Material.PISTON), + 0, 1, 2, + new AltarRecipe(1, 2, 10, new MaterialItemPredicate(Material.SLIME_BLOCK)), + new AltarRecipe(2, 4, 10, new MaterialItemPredicate(Material.PISTON)) + ); + public static VanillaEnchantmentWrapper BLAST_PROTECTION = + new VanillaEnchantmentWrapper(Enchantment.BLAST_PROTECTION, () -> new ItemStack(Material.TNT), + 1, 2, 3, + new AltarRecipe(1, 1, 32, new MaterialItemPredicate(Material.COPPER_INGOT)), + new AltarRecipe(2, 2, 32, new MaterialItemPredicate(Material.IRON_INGOT)), + new AltarRecipe(3, 3, 16, new MaterialItemPredicate(Material.TNT)), + new AltarRecipe(4, 4, 32, new MaterialItemPredicate(Material.TNT)) + ); + public static VanillaEnchantmentWrapper PROJECTILE_PROTECTION = + new VanillaEnchantmentWrapper(Enchantment.PROJECTILE_PROTECTION, () -> new ItemStack(Material.ARROW), + 1, 2, 3, + new AltarRecipe(1, 1, 32, new MaterialItemPredicate(Material.COPPER_INGOT)), + new AltarRecipe(2, 2, 32, new MaterialItemPredicate(Material.IRON_INGOT)), + new AltarRecipe(3, 3, 16, new MaterialItemPredicate(Material.ARROW)), + new AltarRecipe(4, 4, 32, new MaterialItemPredicate(Material.ARROW)) + ); + public static VanillaEnchantmentWrapper PROTECTION = + new VanillaEnchantmentWrapper(Enchantment.PROTECTION, () -> new ItemStack(Material.DIAMOND_CHESTPLATE), + 2, 3, 4, + new AltarRecipe(1, 2, 32, new MaterialItemPredicate(Material.COPPER_INGOT)), + new AltarRecipe(2, 4, 32, new MaterialItemPredicate(Material.IRON_INGOT)), + new AltarRecipe(3, 6, 32, new MaterialItemPredicate(Material.GOLD_INGOT)), + new AltarRecipe(4, 8, 16, new MaterialItemPredicate(Material.DIAMOND)) + ); + public static VanillaEnchantmentWrapper AQUA_AFFINITY = + new VanillaEnchantmentWrapper(Enchantment.AQUA_AFFINITY, () -> new ItemStack(Material.PRISMARINE), + 1, + new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.PRISMARINE_SHARD)) + ); + public static VanillaEnchantmentWrapper SMITE = + new VanillaEnchantmentWrapper(Enchantment.SMITE, () -> new ItemStack(Material.ROTTEN_FLESH), + 1, 2, 3, + new AltarRecipe(1, 1, 10, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(2, 2, 20, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(3, 3, 30, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(4, 4, 40, new MaterialItemPredicate(Material.ROTTEN_FLESH)), + new AltarRecipe(5, 5, 50, new MaterialItemPredicate(Material.ROTTEN_FLESH)) + ); + public static VanillaEnchantmentWrapper POWER = + new VanillaEnchantmentWrapper(Enchantment.POWER, () -> new ItemStack(Material.BOW), + 1, 2, 3, + new AltarRecipe(1, 2, 32, new MaterialItemPredicate(Material.REDSTONE)), + new AltarRecipe(2, 4, 16, new MaterialItemPredicate(Material.REDSTONE_BLOCK)), + new AltarRecipe(3, 6, 16, new MaterialItemPredicate(Material.BLAZE_POWDER)), + new AltarRecipe(4, 8, 8, new MaterialItemPredicate(Material.DIAMOND)), + new AltarRecipe(5, 10, new PotionItemPredicate(PotionEffectType.STRENGTH)) + ); + public static VanillaEnchantmentWrapper THORNS = + new VanillaEnchantmentWrapper(Enchantment.THORNS, () -> new ItemStack(Material.PUFFERFISH), + 0, 1, 2, + new AltarRecipe(1, 1, 16, new MaterialItemPredicate(Material.CACTUS)), + new AltarRecipe(2, 2, 16, new MaterialItemPredicate(Material.SWEET_BERRIES)), + new AltarRecipe(3, 4, new MaterialItemPredicate(Material.DIAMOND_SWORD)) + ); + public static VanillaEnchantmentWrapper LOOTING = + new VanillaEnchantmentWrapper(Enchantment.LOOTING, () -> new ItemStack(Material.EMERALD), + 0, 1, 2, + new AltarRecipe(1, 2, 16, new MaterialItemPredicate(Material.CHEST)), + new AltarRecipe(2, 4, 16, new MaterialItemPredicate(Material.EMERALD)), + new AltarRecipe(3, 8, 10, new MaterialItemPredicate(Material.DIAMOND)) + ); + public static VanillaEnchantmentWrapper FEATHER_FALLING = + new VanillaEnchantmentWrapper(Enchantment.FEATHER_FALLING, () -> new ItemStack(Material.FEATHER), + 0, 1, 2, + new AltarRecipe(1, 2, 10, new MaterialItemPredicate(Material.FEATHER)), + new AltarRecipe(2, 4, 5, new MaterialItemPredicate(Material.HAY_BLOCK)), + new AltarRecipe(3, 6, new ColorlessItemPredicate(Material.WHITE_BED)), + new AltarRecipe(4, 8, 32, new ColorlessItemPredicate(Material.WHITE_WOOL)) + ); + public static VanillaEnchantmentWrapper FIRE_ASPECT = + new VanillaEnchantmentWrapper(Enchantment.FIRE_ASPECT, () -> new ItemStack(Material.BLAZE_POWDER), + 0, 1, 2, + new AltarRecipe(1, 4, 16, new MaterialItemPredicate(Material.BLAZE_POWDER)), + new AltarRecipe(2, 8, 32, new MaterialItemPredicate(Material.BLAZE_POWDER)) + ); + public static VanillaEnchantmentWrapper UNBREAKING = + new VanillaEnchantmentWrapper(Enchantment.UNBREAKING, () -> new ItemStack(Material.BEDROCK), + 0, 1, 2, + new AltarRecipe(1, 2, 32, new MaterialItemPredicate(Material.IRON_INGOT)), + new AltarRecipe(2, 4, 32, new MaterialItemPredicate(Material.OBSIDIAN)), + new AltarRecipe(3, 8, 2, new MaterialItemPredicate(Material.NETHERITE_SCRAP)) + ); + public static VanillaEnchantmentWrapper CHANNELING = + new VanillaEnchantmentWrapper(Enchantment.CHANNELING, () -> new ItemStack(Material.LIGHTNING_ROD), + 0,0,1, + new AltarRecipe(1, 8, 1, new MaterialItemPredicate(Material.LIGHTNING_ROD)) + ); + public static VanillaEnchantmentWrapper LURE = + new VanillaEnchantmentWrapper(Enchantment.LURE, () -> new ItemStack(Material.TRIPWIRE_HOOK), + 1, 2, 3, + new AltarRecipe(1, 1, 16, new MaterialItemPredicate(Material.STRING)), + new AltarRecipe(2, 2, 16, new MaterialItemPredicate(Material.COD)), + new AltarRecipe(3, 4, 16, new MaterialItemPredicate(Material.PUFFERFISH)) + ); + public static VanillaEnchantmentWrapper INFINITY = + new VanillaEnchantmentWrapper(Enchantment.INFINITY, () -> new ItemStack(Material.CHORUS_FLOWER), + 0, + new AltarRecipe(1, 8, 64, new MaterialItemPredicate(Material.ARROW)) + ); + public static VanillaEnchantmentWrapper EFFICIENCY = + new VanillaEnchantmentWrapper(Enchantment.EFFICIENCY, () -> new ItemStack(Material.REDSTONE), + 1, 2, 3, + new AltarRecipe(1, 2, 32, new MaterialItemPredicate(Material.SUGAR)), + new AltarRecipe(2, 4, 32, new MaterialItemPredicate(Material.REDSTONE)), + new AltarRecipe(3, 6, 16, new MaterialItemPredicate(Material.REDSTONE_BLOCK)), + new AltarRecipe(4, 8, 32, new MaterialItemPredicate(Material.GOLD_INGOT)), + new AltarRecipe(5, 10, new PotionItemPredicate(PotionEffectType.SPEED)) + ); + public static VanillaEnchantmentWrapper DEPTH_STRIDER = + new VanillaEnchantmentWrapper(Enchantment.DEPTH_STRIDER, () -> new ItemStack(Material.COD), + 0, 1, 2, + new AltarRecipe(1, 1, new ColorlessItemPredicate(Material.OAK_BOAT)), + new AltarRecipe(2, 2, new MaterialItemPredicate(Material.MINECART)), + new AltarRecipe(3, 4, 3, new MaterialItemPredicate(Material.RABBIT_FOOT)) + ); + public static VanillaEnchantmentWrapper KNOCKBACK = + new VanillaEnchantmentWrapper(Enchantment.KNOCKBACK, () -> new ItemStack(Material.PISTON), + 0, 1, 2, + new AltarRecipe(1, 2, 10, new MaterialItemPredicate(Material.SLIME_BLOCK)), + new AltarRecipe(2, 4, 10, new MaterialItemPredicate(Material.PISTON)) + ); + public static VanillaEnchantmentWrapper SOUL_SPEED = + new VanillaEnchantmentWrapper(Enchantment.SOUL_SPEED, () -> new ItemStack(Material.SOUL_SAND), + 0, 1, 2, + new AltarRecipe(1, 1, 16, new MaterialItemPredicate(Material.SOUL_SAND)), + new AltarRecipe(2, 2, 16, new MaterialItemPredicate(Material.SOUL_SOIL)), + new AltarRecipe(3, 4, 32, new MaterialItemPredicate(Material.SOUL_LANTERN)) + ); + public static VanillaEnchantmentWrapper SWIFT_SNEAK = + new VanillaEnchantmentWrapper(Enchantment.SWIFT_SNEAK, () -> new ItemStack(Material.ECHO_SHARD), + 0, 1, 2, + new AltarRecipe(1, 1, 16, new ColorlessItemPredicate(Material.WHITE_WOOL)), + new AltarRecipe(2, 2, 16, new MaterialItemPredicate(Material.SCULK)), + new AltarRecipe(3, 4, 4, new MaterialItemPredicate(Material.ECHO_SHARD)) + ); + public static VanillaEnchantmentWrapper BANE_OF_ARTHROPODS = + new VanillaEnchantmentWrapper(Enchantment.BANE_OF_ARTHROPODS, () -> new ItemStack(Material.SPIDER_EYE), + 1, 2, 3, + new AltarRecipe(1, 1, 10, new MaterialItemPredicate(Material.SPIDER_EYE)), + new AltarRecipe(2, 2, 20, new MaterialItemPredicate(Material.SPIDER_EYE)), + new AltarRecipe(3, 3, 30, new MaterialItemPredicate(Material.SPIDER_EYE)), + new AltarRecipe(4, 4, 40, new MaterialItemPredicate(Material.SPIDER_EYE)), + new AltarRecipe(5, 5, 50, new MaterialItemPredicate(Material.SPIDER_EYE)) + ); + public static VanillaEnchantmentWrapper WIND_BURST = + new VanillaEnchantmentWrapper(Enchantment.WIND_BURST, () -> new ItemStack(Material.WIND_CHARGE), + 0, 1, 2, + new AltarRecipe(1, 1, 16, new MaterialItemPredicate(Material.WIND_CHARGE)), + new AltarRecipe(2, 2, 32, new MaterialItemPredicate(Material.BREEZE_ROD)), + new AltarRecipe(3, 4, new PotionItemPredicate(PotionEffectType.WIND_CHARGED)) + ); + public static VanillaEnchantmentWrapper BREACH = + new VanillaEnchantmentWrapper(Enchantment.BREACH, () -> new ItemStack(Material.GRINDSTONE), + 1, 2, 3, + new AltarRecipe(1, 2, 16, new MaterialItemPredicate(Material.GRINDSTONE)), + new AltarRecipe(2, 4, 8, new MaterialItemPredicate(Material.ANVIL)), + new AltarRecipe(3, 6, new PotionItemPredicate(PotionEffectType.STRENGTH)), + new AltarRecipe(4, 8, new MaterialItemPredicate(Material.HEAVY_CORE)) + ); + public static VanillaEnchantmentWrapper DENSITY = + new VanillaEnchantmentWrapper(Enchantment.DENSITY, () -> new ItemStack(Material.MACE), + 1, 2, 3, + new AltarRecipe(1, 1, 2, new MaterialItemPredicate(Material.ANVIL)), + new AltarRecipe(2, 2, 4, new MaterialItemPredicate(Material.ANVIL)), + new AltarRecipe(3, 3, 6, new MaterialItemPredicate(Material.ANVIL)), + new AltarRecipe(4, 4, 8, new MaterialItemPredicate(Material.ANVIL)), + new AltarRecipe(5, 5, new MaterialItemPredicate(Material.HEAVY_CORE)) + ); + + private static Set vanilla() { + return Set.of( + MULTISHOT, PIERCING, SILK_TOUCH, VANISHING_CURSE, FROST_WALKER, FORTUNE, BINDING_CURSE, SHARPNESS, SWEEPING_EDGE, + RIPTIDE, QUICK_CHARGE, IMPALING, FIRE_PROTECTION, LOYALTY, LUCK_OF_THE_SEA, RESPIRATION, FLAME, PUNCH, + BLAST_PROTECTION, PROJECTILE_PROTECTION, PROTECTION, AQUA_AFFINITY, SMITE, POWER, THORNS, LOOTING, + FEATHER_FALLING, FIRE_ASPECT, UNBREAKING, CHANNELING, LURE, INFINITY, EFFICIENCY, DEPTH_STRIDER, KNOCKBACK, + SOUL_SPEED, SWIFT_SNEAK, BANE_OF_ARTHROPODS, WIND_BURST, BREACH, DENSITY + ); + } + + public static Set list() { + Set wrappers = new HashSet<>(CustomEnchantments.list()); + + wrappers.addAll(vanilla()); + + wrappers.removeIf(EnchantmentWrapper::isTreasure); + + return wrappers; + } + + public static @Nullable EnchantmentWrapper getByKey(NamespacedKey key) { + for(EnchantmentWrapper curr : list()) { + if(curr.getKey().equals(key)) { + return curr; + } + } + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/PaperEnchantmentTarget.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/PaperEnchantmentTarget.java new file mode 100644 index 0000000..69195ef --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/PaperEnchantmentTarget.java @@ -0,0 +1,48 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import org.bukkit.Material; +import org.bukkit.enchantments.EnchantmentTarget; +import org.jetbrains.annotations.NotNull; + +public enum PaperEnchantmentTarget implements CustomEnchantmentTarget { + ALL(EnchantmentTarget.ALL), + ARMOR(EnchantmentTarget.ARMOR), + ARMOR_FFET(EnchantmentTarget.ARMOR_FEET), + ARMOR_HEAD(EnchantmentTarget.ARMOR_HEAD), + ARMOR_LEGS(EnchantmentTarget.ARMOR_LEGS), + ARMOR_TORSO(EnchantmentTarget.ARMOR_TORSO), + TOOL(EnchantmentTarget.TOOL), + BOW(EnchantmentTarget.BOW), + BREAKABLE(EnchantmentTarget.BREAKABLE), + CROSSBOW(EnchantmentTarget.CROSSBOW), + FISHING_ROD(EnchantmentTarget.FISHING_ROD), + TRIDENT(EnchantmentTarget.TRIDENT), + VANISHABLE(EnchantmentTarget.VANISHABLE), + WEAPON(EnchantmentTarget.WEAPON), + WEARABLE(EnchantmentTarget.WEARABLE); + + final EnchantmentTarget paperTarget; + + PaperEnchantmentTarget(EnchantmentTarget target) { + paperTarget = target; + } + + public boolean includes(@NotNull Material item) { + return paperTarget.includes(item); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/VanillaEnchantmentWrapper.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/VanillaEnchantmentWrapper.java new file mode 100644 index 0000000..9da019c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/VanillaEnchantmentWrapper.java @@ -0,0 +1,172 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys; + +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarRecipe; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +public class VanillaEnchantmentWrapper implements EnchantmentWrapper { + private final Enchantment enchantment; + private final Supplier icon; + private final List recipes; + private final List altarLevels; + + public VanillaEnchantmentWrapper(Enchantment enchantment, Supplier icon, int tier1, int tier2, int tier3, AltarRecipe... recipes) { + this.enchantment = enchantment; + this.icon = icon; + this.altarLevels = List.of(tier1, tier2, tier3); + this.recipes = List.of(recipes); + } + + public VanillaEnchantmentWrapper(Enchantment enchantment, Supplier icon, int tier1, int tier2, AltarRecipe... recipes) { + this(enchantment, icon, tier1, tier2, tier2, recipes); + } + + public VanillaEnchantmentWrapper(Enchantment enchantment, Supplier icon, int tier, AltarRecipe... recipes) { + this(enchantment, icon, tier, tier, recipes); + } + + @Override + public ItemStack apply(ItemStack tool, int level) { + ItemStack result = tool.clone(); + + result.addUnsafeEnchantment(enchantment, level); + + return result; + } + + @Override + public int getLevel(ItemStack tool) { + return tool.getEnchantmentLevel(enchantment); + } + + @Override + public int getMaxLevel() { + return enchantment.getMaxLevel(); + } + + @Override + public boolean canEnchantItem(ItemStack tool) { + Map customEnchantmentLevels = EnchantmentHelper.getCustomEnchantments(tool); + + for(Map.Entry entry : customEnchantmentLevels.entrySet()) { + if(entry.getKey().conflictsWith(enchantment)) { + return false; + } + } + + if(tool.getItemMeta() != null && tool.getItemMeta().hasConflictingEnchant(enchantment)) { + for(Map.Entry entry : tool.getItemMeta().getEnchants().entrySet()) { + if(entry.getKey().equals(enchantment)) { + continue; + } + if(entry.getKey().conflictsWith(enchantment)) { + return false; + } + } + } + + if(tool.getItemMeta() instanceof EnchantmentStorageMeta esm) { + if(esm.hasConflictingStoredEnchant(enchantment)) { + return false; + } + } + + return canGoOnItem(tool); + } + + @Override + public boolean canGoOnItem(ItemStack tool) { + return enchantment.canEnchantItem(tool) || tool.getType() == Material.BOOK + || tool.getType() == Material.ENCHANTED_BOOK; + } + + @Override + public boolean conflictsWith(Enchantment other) { + return enchantment.conflictsWith(other) || other.conflictsWith(enchantment); + } + + @Override + public NamespacedKey getKey() { + return enchantment.getKey(); + } + + @Override + public Component getComponent(int level) { + return enchantment.displayName(level); + } + + @Override + public Component getLevelessComponent() { + TextColor color = NamedTextColor.GRAY; + + if(enchantment.isCursed()) { + color = NamedTextColor.RED; + } + + return Component.translatable(enchantment.translationKey()).color(color).decoration(TextDecoration.ITALIC, false); + } + + @Override + public int maxLevelAvailableInAltar(int altarTier) { + if(altarTier < 1) { + return 0; + } + if(altarTier > 3) { + return getMaxLevel(); + } + return altarLevels.get(altarTier-1); + } + + @Override + public ItemStack getPreIcon() { + return icon.get(); + } + + @Override + public boolean isTreasure() { + return enchantment.isTreasure(); + } + + @Override + public AltarRecipe getRecipe(int level) { + if(level <= 0) { + return recipes.getFirst(); + } + + if(level > recipes.size()) { + return recipes.getLast(); + } + + return recipes.get(level-1); + } + + public Enchantment getEnchantment() { + return enchantment; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarInterface.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarInterface.java new file mode 100644 index 0000000..e753498 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarInterface.java @@ -0,0 +1,177 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys.altar; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentTome; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentWrapper; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentWrappers; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.userinterfaces.*; +import de.blazemcworld.blazinggames.utils.TextLocation; +import de.blazemcworld.blazinggames.utils.TomeAltarStorage; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.BlockState; +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import java.util.*; + +public class AltarInterface extends UserInterface { + private static final InputSlot toolSlot = new SingleInputSlot() { + + @Override + public boolean filterItem(ItemStack stack) { + if(!EnchantmentHelper.canEnchantItem(stack)) { + return false; + } + return super.filterItem(stack); + } + }; + private static final InputSlot materialSlot = new InputSlot(); + private static final InputSlot lapisSlot = new InputSlot() { + + @Override + public boolean filterItem(ItemStack stack) { + if(CustomItem.isCustomItem(stack)) { + return false; + } + if(stack.getType() != Material.LAPIS_LAZULI) { + return false; + } + return super.filterItem(stack); + } + }; + + private final BlockState state; + private final Map altars; + private int tier; + + public AltarInterface(BlazingGames plugin, BlockState state) { + super(plugin, "Enchant", 5); + this.state = state; + this.tier = 0; + this.altars = new HashMap<>(); + reloadAltar(); + } + + @Override + public void preload() { + StaticUserInterfaceSlot pane = new StaticUserInterfaceSlot(element(Material.GRAY_STAINED_GLASS_PANE, + BlazingGames.get().key("pane"))); + StaticUserInterfaceSlot tool = new StaticUserInterfaceSlot(element(Material.DIAMOND_PICKAXE, + BlazingGames.get().key("pane"))); + StaticUserInterfaceSlot randomMaterial = new StaticUserInterfaceSlot(element(Material.COPPER_INGOT, + BlazingGames.get().key("pane"))); + StaticUserInterfaceSlot lapisLazuli = new StaticUserInterfaceSlot(element(Material.LAPIS_LAZULI, + BlazingGames.get().key("pane"))); + + int index = 0; + for(int y = 0; y < 5; y++) { + addSlot(0, y, pane); + addSlot(1, y, pane); + addSlot(2, y, pane); + + for(int x = 3; x < 9; x++) { + addSlot(x, y, new EnchantmentSlot(index)); + index++; + } + } + + addSlot(0, 1, tool); + addSlot(0, 2, randomMaterial); + addSlot(0, 3, lapisLazuli); + + addSlot(1, 1, toolSlot); + addSlot(1, 2, materialSlot); + addSlot(1, 3, lapisSlot); + } + + @Override + public void tick(Player p) { + reloadAltar(); + + reload(); + } + + @Override + public void onClose(Player p) { + for(Map.Entry slot : slots.entrySet()) { + if(slot.getValue() instanceof UsableInterfaceSlot) { + for(ItemStack overflow : p.getInventory().addItem(getItem(slot.getKey())).values()) { + p.getWorld().dropItemNaturally(p.getLocation(), overflow); + } + } + } + } + + private void reloadAltar() { + tier = AltarOfEnchanting.altar.match(state.getLocation()); + + if(tier <= 0) { + getInventory().close(); + return; + } + + List newAltars = TomeAltarStorage.getNear(state.getLocation(), 5); + + altars.clear(); + + for (Location altar : newAltars) { + ItemStack stack = TomeAltarStorage.getItem(altar); + altars.put(TextLocation.serializeRounded(altar), stack); + } + } + + public Set getAvailable() { + ItemStack tool = getItem(1,1); + + Set result = EnchantmentWrappers.list(); + + result.removeIf((wrapper) -> wrapper.maxLevelAvailableInAltar(tier) <= 0); + + if(altars != null) { + for(ItemStack tome : altars.values()) { + if(CustomItem.getCustomItem(tome) instanceof EnchantmentTome customTome) { + result.add(customTome.getWrapper()); + } + } + } + + result.removeIf((wrapper) -> !wrapper.canEnchantItem(tool)); + + return result; + } + + public ItemStack getTool() { + return getItem(1,1); + } + + public ItemStack getLapis() { + return getItem(1,3); + } + + public ItemStack getMaterial() { + return getItem(1,2); + } + + public int getTier() { + return tier; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarOfEnchanting.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarOfEnchanting.java new file mode 100644 index 0000000..fc3bd08 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarOfEnchanting.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys.altar; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.multiblocks.*; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import org.bukkit.Material; +import org.bukkit.util.Vector; + +public class AltarOfEnchanting { + //region Altar MultiBlock + private static final BlockPredicate enchantingTable = new SingleBlockPredicate(Material.ENCHANTING_TABLE); + private static final BlockPredicate smoothQuartz = new SingleBlockPredicate(Material.SMOOTH_QUARTZ); + private static final BlockPredicate obsidian = new SingleBlockPredicate(Material.OBSIDIAN); + private static final BlockPredicate lapisBlock = new SingleBlockPredicate(Material.LAPIS_BLOCK); + private static final BlockPredicate diamondBlock = new SingleBlockPredicate(Material.DIAMOND_BLOCK); + + private static final BlockPredicate quartzStairs = new ComplexBlockPredicate(new SingleBlockPredicate(Material.QUARTZ_STAIRS), + BisectedBlockPredicate.BOTTOM); + + private static final BlockPredicate quartzStairsEast = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.STRAIGHT_EAST); + private static final BlockPredicate quartzStairsWest = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.STRAIGHT_WEST); + private static final BlockPredicate quartzStairsNorth = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.STRAIGHT_NORTH); + private static final BlockPredicate quartzStairsSouth = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.STRAIGHT_SOUTH); + + private static final BlockPredicate quartzStairsNorthEast = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.OUTER_NORTH_EAST); + private static final BlockPredicate quartzStairsNorthWest = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.OUTER_NORTH_WEST); + private static final BlockPredicate quartzStairsSouthEast = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.OUTER_SOUTH_EAST); + private static final BlockPredicate quartzStairsSouthWest = new ComplexBlockPredicate(quartzStairs, StairShapeBlockPredicate.OUTER_SOUTH_WEST); + + public static final MultiBlockStructureMetadata altar = + new MultiBlockStructureMetadata( + BlazingGames.get().key("altar_of_enchanting"), + "Altar of Enchanting", + Style.style(NamedTextColor.DARK_PURPLE), + new MultiLevelBlockStructure( + new MultiBlockStructure() + .add(new Vector(0, 0, 0), enchantingTable), + new MultiBlockStructure() + // base + .addArea(new Vector(-1, -1, -1), new Vector(1, -1, -1), smoothQuartz) + .addArea(new Vector(-1, -1, 1), new Vector(1, -1, 1), smoothQuartz) + .add(new Vector(-1, -1, 0), smoothQuartz) + .add(new Vector(1, -1, 0), smoothQuartz) + // stairs + .addArea(new Vector(-1, -1, -2), new Vector(1, -1, -2), quartzStairsSouth) + .addArea(new Vector(-1, -1, 2), new Vector(1, -1, 2), quartzStairsNorth) + .addArea(new Vector(-2, -1, -1), new Vector(-2, -1, 1), quartzStairsEast) + .addArea(new Vector(2, -1, -1), new Vector(2, -1, 1), quartzStairsWest) + // pillars + .addArea(new Vector(-2, -1, -2), new Vector(-2, 1, -2), obsidian) + .addArea(new Vector(2, -1, 2), new Vector(2, 1, 2), obsidian) + .addArea(new Vector(2, -1, -2), new Vector(2, 1, -2), obsidian) + .addArea(new Vector(-2, -1, 2), new Vector(-2, 1, 2), obsidian), + new MultiBlockStructure() + // stairs + .addArea(new Vector(-2, -2, -3), new Vector(2, -2, -3), quartzStairsSouth) + .addArea(new Vector(-2, -2, 3), new Vector(2, -2, 3), quartzStairsNorth) + .addArea(new Vector(-3, -2, -2), new Vector(-3, -2, 2), quartzStairsEast) + .addArea(new Vector(3, -2, -2), new Vector(3, -2, 2), quartzStairsWest) + // lapis lazuli blocks + .add(new Vector(3, -2, 3), lapisBlock) + .add(new Vector(-3, -2, 3), lapisBlock) + .add(new Vector(3, -2, -3), lapisBlock) + .add(new Vector(-3, -2, -3), lapisBlock), + new MultiBlockStructure() + // upper stairs + .addArea(new Vector(-3, -3, -4), new Vector(3, -3, -4), quartzStairsSouth) + .addArea(new Vector(-3, -3, 4), new Vector(3, -3, 4), quartzStairsNorth) + .addArea(new Vector(-4, -3, -3), new Vector(-4, -3, 3), quartzStairsEast) + .addArea(new Vector(4, -3, -3), new Vector(4, -3, 3), quartzStairsWest) + // corner stairs + .add(new Vector(4, -3, 4), quartzStairsNorthWest) + .add(new Vector(4, -3, -4), quartzStairsSouthWest) + .add(new Vector(-4, -3, 4), quartzStairsNorthEast) + .add(new Vector(-4, -3, -4), quartzStairsSouthEast) + // lower stairs + .addArea(new Vector(-4, -4, -5), new Vector(4, -4, -5), quartzStairsSouth) + .addArea(new Vector(-4, -4, 5), new Vector(4, -4, 5), quartzStairsNorth) + .addArea(new Vector(-5, -4, -4), new Vector(-5, -4, 4), quartzStairsEast) + .addArea(new Vector(5, -4, -4), new Vector(5, -4, 4), quartzStairsWest) + // pillars + .addArea(new Vector(5, -4, 5), new Vector(5, 2, 5), obsidian) + .addArea(new Vector(5, -4, -5), new Vector(5, 2, -5), obsidian) + .addArea(new Vector(-5, -4, 5), new Vector(-5, 2, 5), obsidian) + .addArea(new Vector(-5, -4, -5), new Vector(-5, 2, -5), obsidian) + // diamond blocks + .add(new Vector(5, 3, 5), diamondBlock) + .add(new Vector(5, 3, -5), diamondBlock) + .add(new Vector(-5, 3, 5), diamondBlock) + .add(new Vector(-5, 3, -5), diamondBlock) + )); + //endregion +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarRecipe.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarRecipe.java new file mode 100644 index 0000000..1e7dd0d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/AltarRecipe.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys.altar; + +import de.blazemcworld.blazinggames.items.EmptyItemPredicate; +import de.blazemcworld.blazinggames.items.ItemPredicate; +import org.bukkit.inventory.ItemStack; + +public record AltarRecipe(int lapisAmount, int expAmount, int itemAmount, ItemPredicate itemRequirement) { + public AltarRecipe(int lapisAmount, int expAmount, ItemPredicate itemRequirement) { + this(lapisAmount, expAmount, (itemRequirement instanceof EmptyItemPredicate) ? 0 : 1, itemRequirement); + } + + public boolean matchMaterial(ItemStack material) { + if(!itemRequirement.matchItem(material)) { + return false; + } + + return material.getAmount() >= itemAmount; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/EnchantmentSlot.java b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/EnchantmentSlot.java new file mode 100644 index 0000000..b8a52d7 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/enchantments/sys/altar/EnchantmentSlot.java @@ -0,0 +1,124 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.enchantments.sys.altar; + +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentWrapper; +import de.blazemcworld.blazinggames.userinterfaces.UserInterface; +import de.blazemcworld.blazinggames.userinterfaces.UserInterfaceSlot; + +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.bukkit.advancement.Advancement; +import org.bukkit.advancement.AdvancementProgress; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.Set; + +public class EnchantmentSlot implements UserInterfaceSlot { + private final int index; + + public EnchantmentSlot(int index) { + this.index = index; + } + + @Override + public void onUpdate(UserInterface inventory, int slot) { + if(!(inventory instanceof AltarInterface altarInterface)) { + return; + } + + Set wrappers = altarInterface.getAvailable(); + ItemStack tool = altarInterface.getTool(); + int lapis = altarInterface.getLapis().getAmount(); + ItemStack material = altarInterface.getMaterial(); + + int cur = 0; + for(EnchantmentWrapper wrapper : wrappers) { + if(cur == index) { + inventory.setItem(slot, wrapper.getIcon(tool, lapis, material, altarInterface.getTier())); + return; + } + cur++; + } + + inventory.setItem(slot, ItemStack.empty()); + } + + @Override + public boolean onClick(UserInterface inventory, ItemStack current, ItemStack cursor, int slot, InventoryAction action, boolean isShiftClick, InventoryClickEvent event) { + if(!(inventory instanceof AltarInterface altarInterface)) { + return false; + } + + Set wrappers = altarInterface.getAvailable(); + ItemStack tool = altarInterface.getTool(); + ItemStack lapis = altarInterface.getLapis(); + ItemStack material = altarInterface.getMaterial(); + + int cur = 0; + for(EnchantmentWrapper wrapper : wrappers) { + if(cur == index) { + int level = wrapper.getLevel(tool); + + if(level >= wrapper.getMaxLevel()) { + return false; + } + + if(level >= wrapper.maxLevelAvailableInAltar(altarInterface.getTier())) { + return false; + } + + if(!(event.getWhoClicked() instanceof Player player)) { + return false; + } + + AltarRecipe recipe = wrapper.getRecipe(level+1); + + if(lapis.getAmount() >= recipe.lapisAmount()) { + if(recipe.matchMaterial(material)) { + if(player.getLevel() >= recipe.expAmount()) { + altarInterface.setItem(1, 1, wrapper.apply(tool, level+1)); + lapis.subtract(recipe.lapisAmount()); + material.subtract(recipe.itemAmount()); + player.setLevel(player.getLevel() - recipe.expAmount()); + + player.getWorld().playSound(player, Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1, 1); + + Advancement advancement = Bukkit.getAdvancement(NamespacedKey.fromString("minecraft:story/enchant_item")); + AdvancementProgress progress = player.getAdvancementProgress(advancement); + if (!progress.isDone()) { + progress.getRemainingCriteria().forEach(progress::awardCriteria); + } + + return false; + } + } + } + + player.playSound(player, Sound.ENTITY_SHULKER_HURT_CLOSED, 1, 1); + + return false; + } + cur++; + } + + return false; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/AdvancementEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/AdvancementEventListener.java new file mode 100644 index 0000000..489dcf4 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/AdvancementEventListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.discord.DiscordApp; +import de.blazemcworld.blazinggames.discord.DiscordNotification; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerAdvancementDoneEvent; + +public class AdvancementEventListener implements Listener { + @EventHandler + public void onAdvancementDone(PlayerAdvancementDoneEvent event) { + if (event.getAdvancement().getDisplay() != null && event.getAdvancement().getDisplay().doesAnnounceToChat()) { + DiscordApp.send(DiscordNotification.playerAdvancement( + event.getPlayer(), event.getAdvancement().getDisplay())); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/BlockDestroyEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/BlockDestroyEventListener.java new file mode 100644 index 0000000..977a686 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/BlockDestroyEventListener.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import com.destroystokyo.paper.event.block.BlockDestroyEvent; +import de.blazemcworld.blazinggames.teleportanchor.LodestoneStorage; +import org.bukkit.Material; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; + +public class BlockDestroyEventListener implements Listener { + + @EventHandler + public void onExplosion(BlockDestroyEvent event) { + if (event.getBlock().getType() == Material.LODESTONE) { + LodestoneStorage.destoryLodestone(event.getBlock().getLocation()); + LodestoneStorage.refreshAllInventories(); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/events/BlockExplodeEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/BlockExplodeEventListener.java new file mode 100644 index 0000000..8386f5d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/BlockExplodeEventListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.teleportanchor.LodestoneStorage; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockExplodeEvent; + +import java.util.List; + +public class BlockExplodeEventListener implements Listener { + + @EventHandler + public void onExplosion(BlockExplodeEvent event) { + List blocks = event.blockList(); + for (Block block : blocks) { + if (block.getType() == Material.LODESTONE) { + LodestoneStorage.destoryLodestone(block.getLocation()); + LodestoneStorage.refreshAllInventories(); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/BlockPlaceEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/BlockPlaceEventListener.java new file mode 100644 index 0000000..ecd6c1b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/BlockPlaceEventListener.java @@ -0,0 +1,313 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.BootedComputer; +import de.blazemcworld.blazinggames.computing.ComputerRegistry; +import de.blazemcworld.blazinggames.computing.types.ComputerTypes; +import de.blazemcworld.blazinggames.crates.CrateManager; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.items.CustomItems; +import de.blazemcworld.blazinggames.items.CustomSlabs; +import de.blazemcworld.blazinggames.utils.TextLocation; +import de.blazemcworld.blazinggames.utils.TomeAltarStorage; +import net.kyori.adventure.text.Component; +import org.bukkit.*; +import org.bukkit.attribute.Attribute; +import org.bukkit.block.BlockFace; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; +import org.bukkit.block.data.Orientable; +import org.bukkit.block.structure.StructureRotation; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPlaceEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BlockStateMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; + +public class BlockPlaceEventListener implements Listener { + + @EventHandler + public void onBlockPlace(BlockPlaceEvent event) throws IOException { + if (event.getItemInHand() != null && (CustomItems.SKELETON_KEY.matchItem(event.getItemInHand()) || CrateManager.getKeyULID(event.getItemInHand()) != null)) { + event.setCancelled(true); + return; + } + + if (event.getItemInHand().getType() == Material.SPAWNER) { + CreatureSpawner spawner = (CreatureSpawner) event.getBlock().getState(); + CreatureSpawner item = (CreatureSpawner) ((BlockStateMeta) event.getItemInHand().getItemMeta()).getBlockState(); + + spawner.setSpawnedType(item.getSpawnedType()); + spawner.setSpawnCount(item.getSpawnCount()); + spawner.setDelay(item.getDelay()); + spawner.setMaxNearbyEntities(item.getMaxNearbyEntities()); + spawner.setMinSpawnDelay(item.getMinSpawnDelay()); + spawner.setMaxSpawnDelay(item.getMaxSpawnDelay()); + spawner.setRequiredPlayerRange(item.getRequiredPlayerRange()); + spawner.setSpawnRange(item.getSpawnRange()); + if (item.getPersistentDataContainer().getOrDefault(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, false)) + spawner.getPersistentDataContainer().set(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, true); + + spawner.update(); + } else if (CustomItems.TOME_ALTAR.matchItem(event.getItemInHand())) { + event.setCancelled(true); + event.getItemInHand().setAmount(event.getItemInHand().getAmount() - 1); + TomeAltarStorage.addTomeAltar(event.getBlock().getLocation()); + + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> { + event.getBlock().getLocation().getBlock().setType(Material.BARRIER); + + Location loc = event.getBlock().getLocation().toCenterLocation(); + loc.setY(loc.getY() - 0.5); + loc.setX(loc.getX() - 0.3125); + loc.setZ(loc.getZ() - 0.3125); + + BlockData basePlateBlock = Material.POLISHED_DEEPSLATE.createBlockData(); + BlockDisplay basePlate = (BlockDisplay) event.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + basePlate.setBlock(basePlateBlock); + Transformation transformation = new Transformation(new Vector3f(), new Quaternionf(),new Vector3f(0.625f, 0.0625f, 0.625f),new Quaternionf()); + basePlate.setTransformation(transformation); + + loc.setY(loc.getY() + 0.0625); + loc.setX(loc.getX() + 0.125); + loc.setZ(loc.getZ() + 0.125); + BlockData mainPillarBlock = Material.POLISHED_BLACKSTONE.createBlockData(); + BlockDisplay mainPillar = (BlockDisplay) event.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + mainPillar.setBlock(mainPillarBlock); + transformation = new Transformation(new Vector3f(), new Quaternionf(),new Vector3f(0.375f, 0.625f, 0.375f),new Quaternionf()); + mainPillar.setTransformation(transformation); + + BlockData subPillarBlock = Material.POLISHED_BLACKSTONE_BRICKS.createBlockData(); + + loc.setY(loc.getY() + 0.5625); + loc.setX(loc.getX() - 0.0625); + loc.setZ(loc.getZ() - 0.0625); + BlockDisplay subPillar = (BlockDisplay) event.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + subPillar.setBlock(subPillarBlock); + transformation = new Transformation(new Vector3f(), new Quaternionf(),new Vector3f(0.125f, 0.25f, 0.125f),new Quaternionf()); + subPillar.setTransformation(transformation); + + loc.setX(loc.getX() + 0.375); + subPillar = (BlockDisplay) event.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + subPillar.setBlock(subPillarBlock); + subPillar.setTransformation(transformation); + + loc.setZ(loc.getZ() + 0.375); + subPillar = (BlockDisplay) event.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + subPillar.setBlock(subPillarBlock); + subPillar.setTransformation(transformation); + + loc.setX(loc.getX() - 0.375); + subPillar = (BlockDisplay) event.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + subPillar.setBlock(subPillarBlock); + subPillar.setTransformation(transformation); + }); + } else { + boolean isSlab = CustomItem.getCustomItem(event.getItemInHand()) != null && Objects.requireNonNull(event.getItemInHand().getPersistentDataContainer().get(BlazingGames.get().key("custom_item"), PersistentDataType.STRING)).contains("slab"); + + if (isSlab) { + event.setCancelled(true); + Bukkit.getScheduler().runTaskLater(BlazingGames.get(), () -> { + CustomSlabs.CustomSlab item = (CustomSlabs.CustomSlab) CustomItem.getCustomItem(event.getItemInHand()); + if (item == null) return; + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.getItemInHand().setAmount(event.getItemInHand().getAmount() - 1); + RayTraceResult res = event.getBlock().getWorld().rayTraceBlocks(event.getPlayer().getEyeLocation(), event.getPlayer().getEyeLocation().getDirection(), 5); + if (res == null) return; + Vector vec = res.getHitPosition(); + Location loc = event.getBlock().getLocation().toCenterLocation(); + if (!isTop(vec.getY())) loc.add(0, -0.5, 0); + placeCustomSlab(item, loc, event.getPlayer().getEyeLocation(), res.getHitBlockFace()); + }, 1); + } + } + + ItemStack handItem = event.getItemInHand(); + if (handItem.hasItemMeta()) { + PersistentDataContainer container = handItem.getItemMeta().getPersistentDataContainer(); + String computerTypeString = container.getOrDefault(ComputerRegistry.NAMESPACEDKEY_COMPUTER_TYPE, PersistentDataType.STRING, ""); + if (!computerTypeString.isEmpty()) { + event.setCancelled(true); + + ComputerTypes computerTypes; + try { + computerTypes = ComputerTypes.valueOf(computerTypeString); + } catch (IllegalArgumentException ignored) { + event.getPlayer().sendMessage("This computer type doesn't exist?"); + return; + } + + String computerId = container.getOrDefault(ComputerRegistry.NAMESPACEDKEY_COMPUTER_ID, PersistentDataType.STRING, ""); + Location placeLocation = event.getBlockPlaced().getLocation(); + UUID uuid = event.getPlayer().getUniqueId(); + if (event.getItemInHand().getAmount() > 1) { + ItemStack newStack = event.getItemInHand(); + newStack.setAmount(event.getItemInHand().getAmount() - 1); + event.getPlayer().getInventory().setItem(event.getHand(), newStack); + } else { + event.getPlayer().getInventory().setItem(event.getHand(), new ItemStack(Material.AIR)); + } + if ("".equals(computerId)) { + ComputerRegistry.placeNewComputer( + placeLocation, + computerTypes, + uuid, + computer -> { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + if (computer == null) { + player.sendActionBar(Component.text("Failed to create a computer, please tell a developer to check the console.")); + } else { + player.sendActionBar( + Component.text("Your new computer (%s) has been created with id %s!".formatted(computer.getMetadata().name, computer.getId())) + ); + } + } + } + ); + } else { + ComputerRegistry.placeComputer( + computerId, + placeLocation, + (result, computer) -> { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + if (result && computer != null) { + player.sendActionBar( + Component.text("The computer (%s) has been placed (id: %s)!".formatted(computer.getMetadata().name, computer.getId())) + ); + } + + if (result && computer == null) { + player.sendActionBar(Component.text("fear me.")); + } + + if (!result && computer != null) { + if (ComputerRegistry.getComputerById(computerId) != null) { + BootedComputer dupe = ComputerRegistry.getComputerById(computerId); + player.sendMessage( + "Duplicate computer has an ID of %s and is at [ %s ]." + .formatted(dupe.getId(), TextLocation.serialize(dupe.getMetadata().location)) + ); + player.sendActionBar(Component.text("A computer with this ID is already present in the world. See chat for details.")); + } else { + player.sendActionBar(Component.text("A computer with this location is already present here.")); + } + } + + if (!result && computer == null) { + player.sendActionBar(Component.text("Failed to place that computer, please tell a developer to check the console.")); + } + } + } + ); + } + } + } + } + + private static void summonArmorstand(Location loc, String slab, UUID uuid) { + Shulker shulker = (Shulker) loc.getWorld().spawnEntity(loc, EntityType.SHULKER); + shulker.setAI(false); + shulker.setInvisible(true); + shulker.setSilent(true); + shulker.getPersistentDataContainer().set(BlazingGames.get().key("slab_type"), PersistentDataType.STRING, slab); + shulker.getPersistentDataContainer().set(BlazingGames.get().key("slab"), PersistentDataType.STRING, uuid.toString()); + Objects.requireNonNull(shulker.getAttribute(Attribute.GENERIC_SCALE)).setBaseValue(0.5); + + ArmorStand armorStand = (ArmorStand) loc.getWorld().spawnEntity(loc, EntityType.ARMOR_STAND); + armorStand.setInvisible(true); + armorStand.setInvulnerable(true); + armorStand.setCanMove(false); + armorStand.setMarker(true); + armorStand.setGravity(false); + armorStand.setSmall(true); + Objects.requireNonNull(armorStand.getAttribute(Attribute.GENERIC_SCALE)).setBaseValue(0.00001); + armorStand.addPassenger(shulker); + } + + public static void placeCustomSlab(CustomSlabs.CustomSlab item, Location location, Location direction, BlockFace blockFace) { + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> { + Location originalLoc = location.clone(); + boolean isTop = isTop(originalLoc.getY()); + Location loc = originalLoc.clone().toCenterLocation(); + loc.getBlock().setType(Material.MOVING_PISTON); + loc.setX(loc.getX() - 0.5); + loc.setZ(loc.getZ() - 0.5); + loc.setY(loc.getY() - (isTop ? 0 : 0.5)); + + BlockData blockData = item.material.createBlockData(); + BlockDisplay blockDisplay = (BlockDisplay) loc.getBlock().getWorld().spawnEntity(loc, EntityType.BLOCK_DISPLAY); + if (blockData instanceof Directional || blockData instanceof Orientable) { + double yaw = direction.getYaw(); + BlockFace face = BlockFace.SOUTH; + StructureRotation rotation = StructureRotation.CLOCKWISE_90; + if (yaw >= -135 && yaw < -45) { + face = BlockFace.WEST; + rotation = StructureRotation.NONE; + } + if (yaw >= -45 && yaw < 45) { + face = BlockFace.NORTH; + rotation = StructureRotation.COUNTERCLOCKWISE_90; + } + if (yaw >= 45 && yaw < 135) { + face = BlockFace.EAST; + rotation = StructureRotation.CLOCKWISE_180; + } + if (blockData instanceof Directional directional) directional.setFacing(face); + if (blockData instanceof Orientable orientable) { + orientable.setAxis((blockFace == BlockFace.UP || blockFace == BlockFace.DOWN) ? Axis.Y : Axis.X); + orientable.rotate(rotation); + } + } + blockDisplay.setBlock(blockData); + Transformation transformation = new Transformation(new Vector3f(), new Quaternionf(), new Vector3f(1f, 0.5f, 1f), new Quaternionf()); + blockDisplay.setTransformation(transformation); + + loc = loc.add(0.25, 0, 0.25); + summonArmorstand(loc, item.name, blockDisplay.getUniqueId()); + + loc = loc.add(0.5, 0, 0); + summonArmorstand(loc, item.name, blockDisplay.getUniqueId()); + + loc = loc.add(0, 0, 0.5); + summonArmorstand(loc, item.name, blockDisplay.getUniqueId()); + + loc = loc.add(-0.5, 0, 0); + summonArmorstand(loc, item.name, blockDisplay.getUniqueId()); + }); + } + + public static boolean isTop(double number) { + double decimalPart = number - Math.floor(number); + return decimalPart >= 0.5 && decimalPart <= 0.9; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/BreakBlockEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/BreakBlockEventListener.java new file mode 100644 index 0000000..67769fb --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/BreakBlockEventListener.java @@ -0,0 +1,324 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.BootedComputer; +import de.blazemcworld.blazinggames.computing.ComputerRegistry; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.enchantments.PatternEnchantment; +import de.blazemcworld.blazinggames.items.RecipeHelper; +import de.blazemcworld.blazinggames.teleportanchor.LodestoneStorage; +import de.blazemcworld.blazinggames.utils.InventoryUtils; +import de.blazemcworld.blazinggames.utils.ItemUtils; +import de.blazemcworld.blazinggames.utils.Pair; +import org.bukkit.*; +import org.bukkit.block.*; +import org.bukkit.block.data.Waterlogged; +import org.bukkit.block.data.type.Leaves; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockBreakEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.BlockStateMeta; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.Vector; + +import java.util.*; + +public class BreakBlockEventListener implements Listener { + private final Set logs = Set.of( + Material.OAK_LOG, + Material.DARK_OAK_LOG, + Material.ACACIA_LOG, + Material.BIRCH_LOG, + Material.JUNGLE_LOG, + Material.MANGROVE_LOG, + Material.SPRUCE_LOG, + Material.CHERRY_LOG, + Material.WARPED_STEM, + Material.CRIMSON_STEM + ); + + private final Set leaves = Set.of( + Material.OAK_LEAVES, + Material.DARK_OAK_LEAVES, + Material.ACACIA_LEAVES, + Material.BIRCH_LEAVES, + Material.JUNGLE_LEAVES, + Material.MANGROVE_LEAVES, + Material.SPRUCE_LEAVES, + Material.CHERRY_LEAVES, + Material.AZALEA_LEAVES, + Material.FLOWERING_AZALEA_LEAVES, + Material.WARPED_WART_BLOCK, + Material.NETHER_WART_BLOCK + ); + + + @EventHandler(ignoreCancelled = true, priority = EventPriority.HIGHEST) + public void onBreakBlock(BlockBreakEvent event) { + Player player = event.getPlayer(); + BlockFace playerDir = player.getFacing(); + ItemStack mainHand = player.getInventory().getItemInMainHand(); + BlockFace face = event.getPlayer().getTargetBlockFace(6); + + event.setDropItems(false); + + if (event.getBlock().getType() == Material.SPAWNER) { + if (mainHand.getEnchantmentLevel(Enchantment.SILK_TOUCH) > 0 && ( + mainHand.getType() == Material.IRON_PICKAXE || + mainHand.getType() == Material.DIAMOND_PICKAXE || + mainHand.getType() == Material.NETHERITE_PICKAXE + )) { + event.setExpToDrop(0); + } + } + + Collection drops = getBlockDrops(player, event.getBlock()); + + onAnyBlockBreak(event.getBlock()); + + InventoryUtils.collectableDrop(player, event.getBlock().getLocation(), drops); + + if (EnchantmentHelper.hasActiveCustomEnchantment(mainHand, CustomEnchantments.TREE_FELLER)) { + if (logs.contains(event.getBlock().getType())) { + if (player.getFoodLevel() <= 6) { + return; + } + + ItemStack axe = player.getInventory().getItemInMainHand(); + + int treeFeller = EnchantmentHelper.getActiveCustomEnchantmentLevel(axe, CustomEnchantments.TREE_FELLER); + + if (treeFeller <= 0) { + return; + } + + List blocksToBreak = new ArrayList<>(); + blocksToBreak.add(event.getBlock()); + + boolean foundLeaves = false; + + for (int i = 0; i < blocksToBreak.size(); i++) { + Block block = blocksToBreak.get(i); + for (int x = -1; x <= 1; x++) { + for (int z = -1; z <= 1; z++) { + for (int y = -1; y <= 1; y++) { + Block relBlock = block.getRelative(x, 1, z); + + if (leaves.contains(relBlock.getType())) { + if (relBlock.getBlockData() instanceof Leaves leaf) { + if (!leaf.isPersistent()) { + foundLeaves = true; + } + } + } else if (logs.contains(relBlock.getType())) { + if (!blocksToBreak.contains(relBlock)) { + blocksToBreak.add(relBlock); + } + } + } + } + } + } + + if (!foundLeaves) { + return; + } + + Bukkit.getScheduler().runTaskLater(BlazingGames.get(), () -> treeFeller(player, blocksToBreak), 1); + } + } + + int pattern = EnchantmentHelper.getActiveCustomEnchantmentLevel(mainHand, CustomEnchantments.PATTERN); + + if (pattern > 0 && face != null) { + Pair dimensions = PatternEnchantment.dimensions.get(pattern - 1).left2(); + + for (int i = 0; i < dimensions.left; i++) { + int x = -dimensions.left / 2 + i; + for (int j = 0; j < dimensions.right; j++) { + Vector vec = new Vector(0, 0, 0); + if (face.getModY() != 0) { + int y = -dimensions.right / 2 + j; + switch (playerDir) { + case EAST -> vec = new Vector(y, 0, x); + case WEST -> vec = new Vector(-y, 0, -x); + case NORTH -> vec = new Vector(x, 0, -y); + case SOUTH -> vec = new Vector(-x, 0, y); + } + } else { + switch (face) { + case EAST -> vec = new Vector(0, j, -x); + case WEST -> vec = new Vector(0, j, x); + case NORTH -> vec = new Vector(-x, j, 0); + case SOUTH -> vec = new Vector(x, j, 0); + } + } + if (!vec.isZero()) { + fakeBreakBlock(player, event.getBlock().getLocation().clone().add(vec).getBlock()); + } + } + } + } + } + + private void treeFeller(Player player, List blocksToBreak) { + if (blocksToBreak.isEmpty()) { + return; + } + + if (player.getFoodLevel() <= 0) { + return; + } + + ItemStack axe = player.getInventory().getItemInMainHand(); + + int treeFeller = EnchantmentHelper.getActiveCustomEnchantmentLevel(axe, CustomEnchantments.TREE_FELLER); + + if (treeFeller <= 0) { + return; + } + + Block block = blocksToBreak.getFirst(); + + fakeBreakBlock(player, block); + blocksToBreak.removeFirst(); + + axe = axe.damage(1, player); + + player.getInventory().setItemInMainHand(axe); + + int chance = 100 - treeFeller * 20; + + if (new Random().nextInt(100) + 1 <= chance) { + player.setFoodLevel(player.getFoodLevel() - 1); + } + + Bukkit.getScheduler().runTaskLater(BlazingGames.get(), () -> treeFeller(player, blocksToBreak), 1); + } + + public static Collection getBlockDrops(Player player, Block block) { + ItemStack mainHand = player.getInventory().getItemInMainHand(); + + return getBlockDrops(mainHand, block); + } + + public static Collection getBlockDrops(ItemStack mainHand, Block block) { + BlockState state = block.getState(); + Collection drops = block.getDrops(mainHand); + + if (block.getType() == Material.CHISELED_BOOKSHELF) { + if (mainHand.getEnchantmentLevel(Enchantment.SILK_TOUCH) <= 0) { + drops.add(new ItemStack(Material.CHISELED_BOOKSHELF)); + } + } + + if (block.getType() == Material.SPAWNER) { + if (mainHand.getEnchantmentLevel(Enchantment.SILK_TOUCH) > 0 && ( + mainHand.getType() == Material.IRON_PICKAXE || + mainHand.getType() == Material.DIAMOND_PICKAXE || + mainHand.getType() == Material.NETHERITE_PICKAXE + )) { + CreatureSpawner spawner = (CreatureSpawner) block.getState(); + ItemStack item = new ItemStack(Material.SPAWNER); + BlockStateMeta meta = (BlockStateMeta) item.getItemMeta(); + meta.setBlockState(block.getState()); + if (spawner.getPersistentDataContainer().getOrDefault(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, false)) + meta.getPersistentDataContainer().set(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, true); + item.setItemMeta(meta); + + drops.add(item); + } + } + + if (state instanceof Container && ItemUtils.getUncoloredType(block) != Material.SHULKER_BOX) { + ItemStack[] contents = ((Container) state).getInventory().getContents(); + for (ItemStack item : contents) { + if (item != null && !item.isEmpty()) { + drops.add(item); + } + } + } + + if (EnchantmentHelper.hasActiveCustomEnchantment(mainHand, CustomEnchantments.FLAME_TOUCH)) { + Iterator iter = drops.iterator(); + List smeltedDrops = new ArrayList<>(); + + while(iter.hasNext()) { + ItemStack stack = iter.next(); + + ItemStack smelted = RecipeHelper.smeltItem(stack); + + if(smelted != null) { + iter.remove(); + smeltedDrops.add(smelted); + } + else if(stack.getType().isFuel()) { + iter.remove(); + } + } + + drops.addAll(smeltedDrops); + } + + if (ComputerRegistry.getComputerByLocationRounded(block.getLocation()) != null) { + BootedComputer computer = ComputerRegistry.getComputerByLocationRounded(block.getLocation()); + return List.of(ComputerRegistry.addAttributes(computer.getType().getType().getDisplayItem(computer), computer)); + } else { + return drops; + } + } + + public static void fakeBreakBlock(Player player, Block block) { + if (block.isEmpty() || block.getType().getHardness() < 0 || block.isLiquid()) { + return; + } + + Collection drops = getBlockDrops(player, block); + + onAnyBlockBreak(block); + + block.getWorld().playEffect(block.getLocation(), Effect.STEP_SOUND, block.getBlockData()); + + boolean waterlogged = false; + + if (block.getBlockData() instanceof Waterlogged water) { + waterlogged = water.isWaterlogged(); + } + + block.setType(waterlogged ? Material.WATER : Material.AIR, true); + + InventoryUtils.collectableDrop(player, block.getLocation(), drops); + } + + private static void onAnyBlockBreak(Block block) { + if (block.getType() == Material.LODESTONE) { + LodestoneStorage.destoryLodestone(block.getLocation()); + LodestoneStorage.refreshAllInventories(); + } + + if (ComputerRegistry.getComputerByLocationRounded(block.getLocation()) != null) { + BootedComputer computer = ComputerRegistry.getComputerByLocationRounded(block.getLocation()); + ComputerRegistry.unload(computer.getId()); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/ChatEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/ChatEventListener.java new file mode 100644 index 0000000..2325b9b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/ChatEventListener.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.discord.DiscordApp; +import de.blazemcworld.blazinggames.utils.PlayerConfig; +import de.blazemcworld.blazinggames.utils.TextUtils; +import io.papermc.paper.chat.ChatRenderer; +import io.papermc.paper.event.player.AsyncChatEvent; +import net.kyori.adventure.audience.Audience; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.HoverEvent; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; + +public class ChatEventListener implements Listener, ChatRenderer { + + @EventHandler + public void onChat(AsyncChatEvent event) { + event.renderer(this); // Tell the event to use our renderer + DiscordApp.messageHook(event.getPlayer(), event.message()); + } + + @Override + public @NotNull Component render(@NotNull Player source, @NotNull Component sourceDisplayName, @NotNull Component message, @NotNull Audience viewer) { + // format username + Component username; + PlayerConfig config = PlayerConfig.forPlayer(source.getUniqueId()); + if (config.getDisplayName() != null && !config.getDisplayName().equals(source.getName())) { + username = Component.text(config.getDisplayName()).hoverEvent(HoverEvent.showText(Component.text("Real name: " + source.getName()))); + } else { + username = Component.text(source.getName()).hoverEvent(HoverEvent.showText(Component.text("Real name: " + source.getName()))); + } + + if (config.getNameColor() != null) { + username = username.color(config.getNameColor()); + } + + + if (config.getPronouns() != null) { + username = username.appendSpace().append(Component.text("(" + config.getPronouns() + ")") + .color(NamedTextColor.GRAY).hoverEvent(HoverEvent.showText(Component.text("Pronouns")))); + } + + if (source.isOp()) { + username = username.appendSpace().append(Component.text("\u266E").color(NamedTextColor.RED) + .hoverEvent(HoverEvent.showText(Component.text("Server Operator")))); + } + + // custom formatting + String rawMessage = TextUtils.componentToString(message); + if (meFormat(rawMessage) != null) { + // me when a oneliner needs to be multiline + Component minimalUsername = Component.text(config.getDisplayName() != null ? config.getDisplayName() : source.getName()) + .color(config.getNameColor() != null ? config.getNameColor() : NamedTextColor.WHITE) + .hoverEvent(HoverEvent.showText(Component.text("Real name: " + source.getName()) + .appendNewline().append(Component.text("Pronouns: " + config.getPronouns() != null ? config.getPronouns() : "None specified")) + .appendNewline().append(Component.text("Server Operator: " + (source.isOp() ? "Yes" : "No"))))); + + return Component.text("*").color(NamedTextColor.WHITE) + .appendSpace() + .append(minimalUsername) + .appendSpace() + .append(TextUtils.colorCodeParser(TextUtils.stringToComponent(meFormat(rawMessage))).color(NamedTextColor.WHITE)); + } else if (greentextFormat(rawMessage) != null) { + Component txt = Component.empty().append(username).append(Component.text(": ").color(NamedTextColor.WHITE)); + String[] parts = greentextFormat(rawMessage); + for (String part : parts) { + txt = txt.appendNewline().append(Component.text(" > ").color(NamedTextColor.WHITE)) + .append(TextUtils.colorCodeParser(TextUtils.stringToComponent(part)).color(NamedTextColor.WHITE)); + } + return txt; + } else { + return Component.empty().append(username).append(Component.text(": ").color(NamedTextColor.WHITE)) + .append(TextUtils.colorCodeParser(message).color(NamedTextColor.WHITE)); + } + } + + public static String meFormat(String existingContent) { + if (existingContent.startsWith("* ")) { + return existingContent.substring(1).trim(); + } + return null; + } + + public static String[] greentextFormat(String existingContent) { + if (existingContent.startsWith(">")) { + String[] parts = existingContent.split(">"); + if (parts.length > 1) { + ArrayList output = new ArrayList<>(List.of(parts)); + output.remove(0); + return output.stream().map(String::trim).toArray(String[]::new); + } + } + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/ClickEntityEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/ClickEntityEventListener.java new file mode 100644 index 0000000..cce7ae2 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/ClickEntityEventListener.java @@ -0,0 +1,105 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.items.CustomSlabs; +import de.blazemcworld.blazinggames.utils.Box; +import de.blazemcworld.blazinggames.utils.Face; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; +import org.bukkit.entity.Shulker; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerInteractEntityEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; + +import java.util.Objects; + +public class ClickEntityEventListener implements Listener { + + @EventHandler + public void onClick(PlayerInteractEntityEvent event) { + if (event.getRightClicked() instanceof Villager villager) { + PlayerInventory inventory = event.getPlayer().getInventory(); + if (inventory.getItemInMainHand().getType() != Material.LEAD) return; + event.setCancelled(true); + + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) { + ItemStack itemStack = inventory.getItemInMainHand(); + itemStack.setAmount(itemStack.getAmount() - 1); + if (itemStack.getAmount() == 0) itemStack = new ItemStack(Material.AIR); + inventory.setItemInMainHand(itemStack); + } + + villager.setLeashHolder(event.getPlayer()); + } else if (event.getRightClicked() instanceof Shulker shulker) { + PersistentDataContainer container = shulker.getPersistentDataContainer(); + + ItemStack item = event.getPlayer().getInventory().getItem(event.getHand()); + boolean isSlab = CustomItem.getCustomItem(item) != null && Objects.requireNonNull(item.getPersistentDataContainer().get(BlazingGames.get().key("custom_item"), PersistentDataType.STRING)).contains("slab"); + if (isSlab && container.has(BlazingGames.get().key("slab")) && container.has(BlazingGames.get().key("slab_type"))) { + Location loc = shulker.getLocation(); + Vector vec = new Vector(loc.getX(), loc.getY(), loc.getZ()); + Box box = new Box( + new Face( // up + vec.clone().add(new Vector(-0.25, 0.5, 0.25)), + vec.clone().add(new Vector(0.25, 0.5, -0.25)) + ), + new Face( // down + vec.clone().add(new Vector(-0.25, 0, 0.25)), + vec.clone().add(new Vector(0.25, 0, -0.25)) + ), + new Face( // west + vec.clone().add(new Vector(-0.25, 0, 0.25)), + vec.clone().add(new Vector(-0.25, 0.5, -0.25)) + ), + new Face( // east + vec.clone().add(new Vector(0.25, 0, 0.25)), + vec.clone().add(new Vector(0.25, 0.5, -0.25)) + ), + new Face( // south + vec.clone().add(new Vector(-0.25, 0, 0.25)), + vec.clone().add(new Vector(0.25, 0.5, 0.25)) + ), + new Face( // north + vec.clone().add(new Vector(-0.25, 0, -0.25)), + vec.clone().add(new Vector(0.25, 0.5, -0.25)) + ) + ); + RayTraceResult res = event.getPlayer().rayTraceEntities(5); + if (res == null) return; + Vector dir = box.getDirection(res.getHitPosition()); + BlockFace face = box.getFace(res.getHitPosition()); + if (dir == null || face == null) return; + dir = dir.multiply(0.5); + CustomSlabs.CustomSlab slab = (CustomSlabs.CustomSlab) CustomItem.getCustomItem(item); + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.getPlayer().getInventory().getItem(event.getHand()).setAmount(event.getPlayer().getInventory().getItem(event.getHand()).getAmount() - 1); + BlockPlaceEventListener.placeCustomSlab(slab, loc.add(dir), event.getPlayer().getEyeLocation(), face); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/ClickInventorySlotEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/ClickInventorySlotEventListener.java new file mode 100644 index 0000000..1ad9b66 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/ClickInventorySlotEventListener.java @@ -0,0 +1,284 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.items.CustomItems; +import de.blazemcworld.blazinggames.userinterfaces.UserInterface; +import de.blazemcworld.blazinggames.utils.InventoryUtils; +import de.blazemcworld.blazinggames.utils.Pair; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.HumanEntity; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; + +public class ClickInventorySlotEventListener implements Listener { + @EventHandler + public void onClickSlot(InventoryClickEvent event) { + Inventory inventory = event.getClickedInventory(); + + if(inventory == null) { + return; + } + + if(event.getInventory().getHolder() instanceof UserInterface ui && event.getAction() == InventoryAction.COLLECT_TO_CURSOR) { + event.setCancelled(!ui.onShiftClick(event.getCursor(), event.getAction(), event)); + return; + } + + if(inventory.getHolder() instanceof UserInterface ui) { + event.setCancelled(!ui.onClick(event.getCurrentItem(), event.getCursor(), event.getSlot(), event.getAction(), event)); + return; + } + + if (inventory instanceof PlayerInventory) { + if (event.getClick() == ClickType.RIGHT && CustomItems.PORTABLE_CRAFTING_TABLE.matchItem(event.getCurrentItem())) { + event.setCancelled(true); + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> event.getWhoClicked().openWorkbench(null, true)); + } + + if(!event.isCancelled() && event.getInventory().getHolder() instanceof UserInterface ui && event.getAction() == InventoryAction.MOVE_TO_OTHER_INVENTORY) { + event.setCancelled(!ui.onShiftClick(event.getCurrentItem(), event.getAction(), event)); + return; + } + } + + if(inventory instanceof GrindstoneInventory grindstone) { + ItemStack cursorItem = event.getCursor().clone(); + if(cursorItem.getType() == Material.SPONGE || cursorItem.getType() == Material.WET_SPONGE) { + if(event.getSlot() != 2) { + ItemStack eventItem; + + if(event.getSlot() == 0) { + eventItem = grindstone.getUpperItem(); + } + else if(event.getSlot() == 1) { + eventItem = grindstone.getLowerItem(); + } + else { + return; + } + + if(eventItem == null) { + eventItem = ItemStack.empty(); + } + else { + eventItem = eventItem.clone(); + } + + BlazingGames.get().debugLog(cursorItem.toString()); + BlazingGames.get().debugLog(eventItem.toString()); + + if(event.getClick() == ClickType.LEFT) { + if(eventItem.isSimilar(cursorItem)) { + int total = eventItem.getAmount() + cursorItem.getAmount(); + if(total > eventItem.getMaxStackSize()) { + cursorItem.setAmount(total - eventItem.getMaxStackSize()); + eventItem.setAmount(eventItem.getMaxStackSize()); + } + else { + cursorItem.subtract(cursorItem.getAmount()); + eventItem.setAmount(total); + } + } + else { + ItemStack swap = cursorItem; + cursorItem = eventItem; + eventItem = swap; + } + } + else if(event.getClick() == ClickType.RIGHT) { + if(eventItem.isSimilar(cursorItem)) { + if(eventItem.getAmount() < eventItem.getMaxStackSize()) { + eventItem.add(); + cursorItem.subtract(); + } + } else { + if (eventItem == null || eventItem.isEmpty()) { + eventItem = cursorItem.asOne(); + cursorItem.subtract(); + } + } + } + + BlazingGames.get().log(cursorItem.toString()); + BlazingGames.get().log(eventItem.toString()); + + if(event.getSlot() == 0) { + grindstone.setUpperItem(eventItem); + } + else if(event.getSlot() == 1) { + grindstone.setLowerItem(eventItem); + } + event.getWhoClicked().setItemOnCursor(cursorItem); + + event.setCancelled(true); + return; + } + } + if(event.getSlot() == 2) { + ItemStack up = grindstone.getUpperItem(); + ItemStack down = grindstone.getLowerItem(); + ItemStack result = grindstone.getResult(); + + if(result != null && !result.isEmpty()) { + if(up != null && !up.isEmpty()) { + if(down != null && !down.isEmpty()) { + if(up.getType() == Material.SPONGE || up.getType() == Material.WET_SPONGE) { + ItemStack book = scrubResultClick(down, up); + + if(book == null) { + return; + } + + event.setCancelled(true); + + if(giveItemStack(event, result)) { + grindstone.setLowerItem(book); + } + + return; + } + else if(down.getType() == Material.SPONGE || down.getType() == Material.WET_SPONGE) { + ItemStack book = scrubResultClick(up, down); + + if(book == null) { + return; + } + + event.setCancelled(true); + + if(giveItemStack(event, result)) { + grindstone.setUpperItem(book); + } + + return; + } + } + } + } + } + } + + if(inventory instanceof AnvilInventory anvil) { + if(event.getSlotType() == InventoryType.SlotType.RESULT) { + ItemStack result = anvil.getResult(); + ItemStack book = anvil.getSecondItem(); + + if(book != null && book.getType() == Material.ENCHANTED_BOOK) { + if(book.getItemMeta() instanceof EnchantmentStorageMeta esm) { + if(esm.hasStoredEnchant(Enchantment.INFINITY)) { + if(result != null && result.getType() == Material.FIREWORK_ROCKET + && result.getEnchantmentLevel(Enchantment.INFINITY) > 0) { + if(giveItemStack(event, result)) { + anvil.setFirstItem(ItemStack.empty()); + anvil.setSecondItem(ItemStack.empty()); + anvil.setResult(ItemStack.empty()); + + HumanEntity p = event.getWhoClicked(); + + p.getWorld().playSound(p, Sound.BLOCK_ANVIL_USE, SoundCategory.BLOCKS, 1, 1); + } + } + } + } + } + } + } + } + + private ItemStack scrubResultClick(ItemStack up, ItemStack down) { + ItemStack book = new ItemStack(Material.ENCHANTED_BOOK); + + if(down.getType() == Material.SPONGE) { + Pair enchantment = EnchantmentHelper.getEnchantmentEntryByIndex(up, down.getAmount()); + + if(enchantment == null) { + return null; + } + + if(book.getItemMeta() instanceof EnchantmentStorageMeta meta) { + meta.addStoredEnchant(enchantment.left, enchantment.right, true); + book.setItemMeta(meta); + } + else { + return null; + } + } + if(down.getType() == Material.WET_SPONGE) { + Pair enchantment + = EnchantmentHelper.getCustomEnchantmentEntryByIndex(up, down.getAmount()); + + if(enchantment == null) { + return null; + } + + book = EnchantmentHelper.setCustomEnchantment(book, enchantment.left, enchantment.right); + } + + return book; + } + + private boolean giveItemStack(InventoryClickEvent event, ItemStack stack) { + stack = stack.clone(); + + if(event.isShiftClick()) { + if(InventoryUtils.canFitItem(event.getWhoClicked().getInventory(), stack)) { + InventoryUtils.giveItem(event.getWhoClicked().getInventory(), stack); + return true; + } + return false; + } + else if(event.getClick().isKeyboardClick()) { + ItemStack shouldBeAir = event.getWhoClicked().getInventory().getItem(event.getHotbarButton()); + + if(shouldBeAir == null || shouldBeAir.isEmpty()) { + event.getWhoClicked().getInventory().setItem(event.getHotbarButton(), stack); + return true; + } + return false; + } + else { + if(event.getCursor().isEmpty()) { + event.getWhoClicked().setItemOnCursor(stack); + return true; + } + if(event.getCursor().isSimilar(stack)) { + int total = event.getCursor().getAmount() + stack.getAmount(); + if(total <= stack.getMaxStackSize()) { + stack.setAmount(total); + event.getWhoClicked().setItemOnCursor(stack); + return true; + } + } + return false; + } + } +} + + diff --git a/src/main/java/de/blazemcworld/blazinggames/events/DeathEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/DeathEventListener.java new file mode 100644 index 0000000..9f3afa3 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/DeathEventListener.java @@ -0,0 +1,94 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.crates.CrateManager; +import de.blazemcworld.blazinggames.discord.DiscordApp; +import de.blazemcworld.blazinggames.discord.DiscordNotification; +import de.blazemcworld.blazinggames.utils.TextUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +import java.util.List; + +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PlayerDeathEvent; + +public class DeathEventListener implements Listener { + public static final List LIQUIDS = List.of( + Material.WATER, + Material.LAVA + ); + + public static final List ILLEGALS = List.of( + Material.BEDROCK, + Material.END_PORTAL_FRAME, + Material.NETHER_PORTAL, + Material.MOVING_PISTON, // custom slab compatibility + Material.END_PORTAL + ); + + @EventHandler(ignoreCancelled = true) + public void onDeath(PlayerDeathEvent event) { + event.setKeepLevel(false); + event.setKeepInventory(false); + DiscordApp.send(DiscordNotification.playerDeath(event.getPlayer(), TextUtils.componentToString(event.deathMessage()))); + + Player player = event.getEntity(); + Location deathLocation = event.getEntity().getLocation(); + player.sendMessage(Component.text("You've died at: %s, %s, %s. A crate with your items and exp will spawn nearby." + .formatted(deathLocation.blockX(), deathLocation.blockY(), deathLocation.blockZ())).color(NamedTextColor.RED)); + Location crateLocation = deathLocation.clone(); + World world = crateLocation.getWorld(); + + if (crateLocation.getBlockY() < world.getMinHeight()) { + crateLocation.setY(world.getMinHeight()); + } + + int illegalsTriesRemaining = 20; + while (illegalsTriesRemaining > 0 && ILLEGALS.contains(crateLocation.getBlock().getType())) { + crateLocation = crateLocation.add(0, 1, 0); + illegalsTriesRemaining--; + } + + int liquidsTriesRemaining = 20; + while (liquidsTriesRemaining > 0 && LIQUIDS.contains(crateLocation.getBlock().getType())) { + crateLocation = crateLocation.add(0, 1, 0); + liquidsTriesRemaining--; + } + + if (crateLocation.getBlockY() >= world.getMaxHeight()) { + crateLocation.setY(world.getMaxHeight() - 1); + } + + if (crateLocation.getBlock().getType() != Material.AIR) { + crateLocation.getBlock().breakNaturally(true); + } + + crateLocation.getBlock().setType(Material.END_PORTAL_FRAME); + + final int expDropped = event.getDroppedExp(); + event.setDroppedExp(0); + event.getDrops().clear(); + String ulid = CrateManager.createDeathCrate(player.getUniqueId(), event.getPlayer().getInventory(), expDropped, crateLocation); + event.getItemsToKeep().add(CrateManager.makeKey(ulid, crateLocation)); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/EntityDamagedByEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/EntityDamagedByEventListener.java new file mode 100644 index 0000000..64be78e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/EntityDamagedByEventListener.java @@ -0,0 +1,125 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.ComputerRegistry; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.items.CustomItems; +import de.blazemcworld.blazinggames.utils.InventoryUtils; +import org.bukkit.*; +import org.bukkit.entity.*; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageByEntityEvent; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; + +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.UUID; + +public class EntityDamagedByEventListener implements Listener { + @SuppressWarnings("deprecation") + @EventHandler + public void onEntityDeath(EntityDamageByEntityEvent event) { + Entity victim = event.getEntity(); + Entity damager = event.getDamager(); + + ItemStack weapon = ItemStack.empty(); + + if (damager instanceof Player attacker && ComputerRegistry.getComputerByActorUUID(victim.getUniqueId()) != null) { + event.setCancelled(true); + ComputerRegistry.getComputerByActorUUID(victim.getUniqueId()).damageHookAddHit(attacker); + } + + if(damager instanceof Player p) { + weapon = p.getInventory().getItemInMainHand(); + } + else if(damager instanceof Mob m) { + weapon = m.getEquipment().getItemInMainHand(); + } + + double damageAdded = 0; + for(Map.Entry enchantment : EnchantmentHelper.getActiveCustomEnchantments(weapon).entrySet()) { + damageAdded += enchantment.getKey().getDamageIncrease(victim, enchantment.getValue()); + } + + if (damageAdded != 0) { + if ((victim instanceof Player p && !(p.isBlocking() && event.getDamage(EntityDamageEvent.DamageModifier.BLOCKING) != 0)) || !(victim instanceof Player)) { + event.setDamage(event.getDamage() + damageAdded); + } + } + + if(victim instanceof Player p && damager instanceof Damageable damageable) { + if(p.isBlocking() && event.getDamage(EntityDamageEvent.DamageModifier.BLOCKING) != 0) { + ItemStack shield = p.getActiveItem(); + int reflectiveDefenses = EnchantmentHelper.getActiveCustomEnchantmentLevel(shield, CustomEnchantments.REFLECTIVE_DEFENSES); + + if(reflectiveDefenses != 0 && new Random().nextInt(Math.max(7 - reflectiveDefenses, 2)) == 0) { + double dmg = Math.abs(event.getDamage(EntityDamageEvent.DamageModifier.BLOCKING)) * 0.15; + + if(dmg < 5) dmg = 5; + + damageable.damage(dmg, p); + shield = shield.damage(10, p); + p.getInventory().setItem(p.getActiveItemHand(), shield); + + p.getWorld().playSound(p, Sound.ENCHANT_THORNS_HIT, 1, 1.5f); + } + } + } else if (damager instanceof Player p) { + PersistentDataContainer container = victim.getPersistentDataContainer(); + + if (container.has(BlazingGames.get().key("slab")) && container.has(BlazingGames.get().key("slab_type"))) { + event.setCancelled(true); + UUID displayBlockUUID = UUID.fromString(Objects.requireNonNull(container.get(BlazingGames.get().key("slab"), PersistentDataType.STRING))); + String slabType = container.get(BlazingGames.get().key("slab_type"), PersistentDataType.STRING); + BlockDisplay displayBlock = (BlockDisplay) p.getWorld().getEntity(displayBlockUUID); + + if (displayBlock == null) return; + Location blockLocation = displayBlock.getLocation().toCenterLocation(); + Sound breakSound = displayBlock.getBlock().getSoundGroup().getBreakSound(); + displayBlock.remove(); + + double y = victim.getY(); + p.getWorld().getNearbyEntitiesByType(Shulker.class, blockLocation, 0.5).forEach(shulker -> { + PersistentDataContainer c = shulker.getPersistentDataContainer(); + if (c.has(BlazingGames.get().key("slab")) && c.has(BlazingGames.get().key("slab_type")) && y == shulker.getLocation().getY()) { + Objects.requireNonNull(shulker.getVehicle()).remove(); + shulker.remove(); + } + }); + + CustomItem slab = CustomItems.getByKey(BlazingGames.get().key(slabType + "_slab")); + if (slab == null) return; + if (p.getGameMode() != GameMode.CREATIVE) { + InventoryUtils.collectableDrop(p, blockLocation, slab.create()); + } + p.getWorld().playSound(blockLocation, breakSound, 1, 1); + + if (p.getWorld().getNearbyEntitiesByType(Shulker.class, blockLocation, 0.5).isEmpty()) + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> p.getWorld().getBlockAt(blockLocation).setType(Material.AIR)); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/EntityDeathEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/EntityDeathEventListener.java new file mode 100644 index 0000000..64a2bb4 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/EntityDeathEventListener.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import org.bukkit.Material; +import org.bukkit.Sound; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDeathEvent; +import org.bukkit.inventory.ItemStack; + +import java.util.ArrayList; +import java.util.List; + +public class EntityDeathEventListener implements Listener { + @EventHandler + public void onEntityDeath(EntityDeathEvent event) { + LivingEntity victim = event.getEntity(); + Player killer = victim.getKiller(); + + if(killer == null) { + return; + } + + ItemStack mainHand = killer.getInventory().getItemInMainHand(); + + int capturing = EnchantmentHelper.getActiveCustomEnchantmentLevel(mainHand, CustomEnchantments.CAPTURING); + + if(Math.random() < capturing*0.05) { + Material spawnEgg = Material.getMaterial(victim.getType().getKey().getKey().toUpperCase() + "_SPAWN_EGG"); + + if(spawnEgg != null) { + victim.getWorld().playSound(victim, Sound.ENTITY_ITEM_PICKUP, 1, 0.75f); + event.getDrops().add(new ItemStack(spawnEgg)); + } + } + + int scavenger = EnchantmentHelper.getActiveCustomEnchantmentLevel(mainHand, CustomEnchantments.SCAVENGER); + + if(Math.random() < scavenger*0.01) { + victim.getWorld().playSound(victim, Sound.BLOCK_CHISELED_BOOKSHELF_PICKUP_ENCHANTED, 1, 0.5f); + List extraDrops = new ArrayList<>(); + for(ItemStack stack : event.getDrops()) { + extraDrops.add(stack.clone()); + } + event.getDrops().addAll(extraDrops); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/EntityExplodeEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/EntityExplodeEventListener.java new file mode 100644 index 0000000..92fa158 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/EntityExplodeEventListener.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.teleportanchor.LodestoneStorage; +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityExplodeEvent; + +import java.util.List; + +public class EntityExplodeEventListener implements Listener { + + @EventHandler + public void onExplosion(EntityExplodeEvent event) { + List blocks = event.blockList(); + for (Block block : blocks) { + if (block.getType() == Material.LODESTONE) { + LodestoneStorage.destoryLodestone(block.getLocation()); + LodestoneStorage.refreshAllInventories(); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/InteractEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/InteractEventListener.java new file mode 100644 index 0000000..ddec0f6 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/InteractEventListener.java @@ -0,0 +1,527 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.crates.CrateData; +import de.blazemcworld.blazinggames.crates.CrateManager; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarInterface; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.items.CustomItems; +import de.blazemcworld.blazinggames.items.CustomSlabs; +import de.blazemcworld.blazinggames.utils.InventoryUtils; +import de.blazemcworld.blazinggames.utils.TomeAltarStorage; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.world.level.block.entity.vault.VaultBlockEntity; +import net.minecraft.world.level.block.entity.vault.VaultServerData; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.craftbukkit.block.CraftVault; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.*; +import org.bukkit.event.Event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.EquipmentSlot; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.PlayerInventory; +import org.bukkit.inventory.meta.BundleMeta; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.inventory.meta.FireworkMeta; +import org.bukkit.persistence.PersistentDataContainer; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.util.Transformation; +import org.bukkit.util.Vector; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +public class InteractEventListener implements Listener { + private final Set dirt = Set.of( + Material.DIRT, + Material.ROOTED_DIRT, + Material.COARSE_DIRT, + Material.GRASS_BLOCK, + Material.DIRT_PATH + ); + + @EventHandler + public void onInteract(PlayerInteractEvent event) throws IOException, ClassNotFoundException { + Player player = event.getPlayer(); + ItemStack eventItem = event.getItem(); + EquipmentSlot hand = event.getHand(); + Block block = event.getClickedBlock(); + BlockFace face = event.getBlockFace(); + Location interactionPoint = event.getInteractionPoint(); + Vector clampedInteractionPoint = null; + + if(interactionPoint != null && block != null) { + clampedInteractionPoint = interactionPoint.clone().subtract(block.getLocation()).toVector(); + } + + if (block != null && block.getType() == Material.VAULT) vaultShit(block); + + if (eventItem != null && (CustomItems.SKELETON_KEY.matchItem(eventItem) || CrateManager.getKeyULID(eventItem) != null)) { + event.setCancelled(true); + } + + if (block != null && block.getType() == Material.END_PORTAL_FRAME && event.getAction() == Action.RIGHT_CLICK_BLOCK) { + ItemStack handItem = player.getInventory().getItem(hand); + String ulid = (CustomItems.SKELETON_KEY.matchItem(handItem) || CustomItems.TO_GO_BOX.matchItem(handItem)) + ? CrateManager.getKeyULID(block.getLocation()) : CrateManager.getKeyULID(handItem); + if (CustomItems.SKELETON_KEY.matchItem(handItem) || CustomItems.TO_GO_BOX.matchItem(handItem)) player.setCooldown(handItem.getType(), 200); + if (ulid != null) { + CrateData data = CrateManager.readCrate(ulid); + Location crateLocation = data.location; + + if (CustomItems.TO_GO_BOX.matchItem(handItem)) { + crateLocation.getBlock().breakNaturally(); + ItemStack filledToGoBox = new ItemStack(Material.BUNDLE); + BundleMeta bundleMeta = (BundleMeta) filledToGoBox.getItemMeta(); + + if (data.helmet != null) bundleMeta.addItem(data.helmet); + if (data.chestplate != null) bundleMeta.addItem(data.chestplate); + if (data.leggings != null) bundleMeta.addItem(data.leggings); + if (data.boots != null) bundleMeta.addItem(data.boots); + if (data.offhand != null) bundleMeta.addItem(data.offhand); + for (ItemStack itemStack : data.hotbarItems) { + if (itemStack == null) continue; bundleMeta.addItem(itemStack); + } + for (ItemStack itemStack : data.inventoryItems) { + if (itemStack == null) continue; bundleMeta.addItem(itemStack); + } + + filledToGoBox.setItemMeta(bundleMeta); + player.getInventory().getItem(hand).setAmount(player.getInventory().getItem(hand).getAmount() - 1); + if (player.getInventory().firstEmpty() != -1) { + player.getInventory().addItem(filledToGoBox); + } else { + player.getWorld().dropItemNaturally(player.getLocation(), filledToGoBox); + } + player.giveExp(data.exp); + CrateManager.deleteCrate(ulid); + return; + } + + if (crateLocation != null && ( + crateLocation.blockX() == block.getX() && crateLocation.blockY() == block.getY() && crateLocation.blockZ() == block.getZ() + )) { + PlayerInventory inventory = player.getInventory(); + + if ( + (data.offhand != null && inventory.getItemInOffHand() != null && !inventory.getItemInOffHand().isEmpty()) || + (data.helmet != null && inventory.getHelmet() != null && !inventory.getHelmet().isEmpty()) || + (data.chestplate != null && inventory.getChestplate() != null && !inventory.getChestplate().isEmpty()) || + (data.leggings != null && inventory.getLeggings() != null && !inventory.getLeggings().isEmpty()) || + (data.boots != null && inventory.getBoots() != null && !inventory.getBoots().isEmpty()) + ) { + player.sendActionBar(Component.text("Move your offhand/armor into your inventory to open").color(NamedTextColor.RED)); + return; + } + + if (handItem.getAmount() > 1) { + ItemStack newStack = handItem; + newStack.setAmount(handItem.getAmount() - 1); + inventory.setItem(hand, newStack); + } else { + inventory.setItem(hand, new ItemStack(Material.AIR)); + } + crateLocation.getBlock().breakNaturally(true); + + if (data.offhand != null) inventory.setItemInOffHand(data.offhand); + if (data.helmet != null) inventory.setHelmet(data.helmet); + if (data.chestplate != null) inventory.setChestplate(data.chestplate); + if (data.leggings != null) inventory.setLeggings(data.leggings); + if (data.boots != null) inventory.setBoots(data.boots); + + int hotbarIndex = -1; + for (ItemStack hotbarItem : data.hotbarItems) { + hotbarIndex++; + if (hotbarItem == null) continue; + if (inventory.getItem(hotbarIndex) == null) { + inventory.setItem(hotbarIndex, hotbarItem); + } else { + if (inventory.firstEmpty() == -1) { + crateLocation.getWorld().dropItemNaturally(crateLocation, hotbarItem); + } + inventory.addItem(hotbarItem); + } + } + + int inventoryIndex = 8; + for (ItemStack inventoryItem : data.inventoryItems) { + inventoryIndex++; + if (inventoryItem == null) continue; + if (inventory.getItem(inventoryIndex) == null) { + inventory.setItem(inventoryIndex, inventoryItem); + } else { + if (inventory.firstEmpty() == -1) { + crateLocation.getWorld().dropItemNaturally(crateLocation, inventoryItem); + } + inventory.addItem(inventoryItem); + } + } + + player.giveExp(data.exp); + + CrateManager.deleteCrate(ulid); + } + } + return; + } + + if(block != null && event.getAction() == Action.RIGHT_CLICK_BLOCK && event.useInteractedBlock() != Event.Result.DENY) { + if (block.getType() == Material.ENCHANTING_TABLE) { + player.openInventory(new AltarInterface(BlazingGames.get(), block.getState()).getInventory()); + event.setCancelled(true); + } + } + + if (CustomItems.PORTABLE_CRAFTING_TABLE.matchItem(eventItem)) { + event.setCancelled(true); + player.openWorkbench(null, true); + return; + } + + if (player.isSneaking() && eventItem != null && hand != null && eventItem.getType() == Material.GLASS_BOTTLE) { + Block target = event.getPlayer().getTargetBlockExact(5, FluidCollisionMode.ALWAYS); + if (target != null && target.getType() == Material.WATER) return; + if (player.getLevel() > 0) { + event.setCancelled(true); + player.setLevel(player.getLevel() - 1); + if (player.getInventory().getItem(hand).getAmount() == 1) { + player.getInventory().setItem(hand, new ItemStack(Material.EXPERIENCE_BOTTLE, 1)); + } else { + player.getInventory().getItem(hand).subtract(1); + HashMap remaining = event.getPlayer().getInventory().addItem(new ItemStack(Material.EXPERIENCE_BOTTLE)); + for (ItemStack item : remaining.values()) { + event.getPlayer().getWorld().dropItem(event.getPlayer().getLocation(), item); + } + } + event.getPlayer().getWorld().playSound(event.getPlayer().getLocation(), Sound.BLOCK_BREWING_STAND_BREW, 1, 1); + } + } + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null && hand != null && eventItem != null) { + if(EnchantmentHelper.hasActiveCustomEnchantment(eventItem, CustomEnchantments.NATURE_BLESSING)) { + if(!dirt.contains(block.getType()) || player.isSneaking()) { + if(block.applyBoneMeal(event.getBlockFace())) { + eventItem = eventItem.damage(1, player); + + player.getInventory().setItem(hand, eventItem); + } + } + } + if(CustomItems.BUILDER_WAND.matchItem(eventItem)) { + if(!player.hasCooldown(Material.BLAZE_ROD)) { + int blocksUsed = CustomItems.BUILDER_WAND.build(player, eventItem, block, face, clampedInteractionPoint); + if(blocksUsed > 0) { + player.setCooldown(Material.BLAZE_ROD, 5); + player.getWorld().playSound(player, Sound.ENTITY_CHICKEN_STEP, 1, 1.25f); + } + } + } + if(CustomItems.BLUEPRINT.matchItem(eventItem)) { + if(!player.hasCooldown(Material.PAPER)) { + event.setCancelled(true); + CustomItems.BLUEPRINT.outputMultiBlockProgress(player, block.getLocation()); + player.setCooldown(Material.PAPER, 40); + } + } + if (block.getType() == Material.SPAWNER) + spawnerInteractions(player, hand, eventItem, (CreatureSpawner) block.getState()); + } + + if(event.getAction().isLeftClick() && hand != null) { + if(player.isSneaking() && CustomItems.BUILDER_WAND.matchItem(eventItem)) { + event.setCancelled(true); + + eventItem = CustomItems.BUILDER_WAND.cycleMode(eventItem); + + player.getInventory().setItem(hand, eventItem); + + player.sendActionBar(Component.text(CustomItems.BUILDER_WAND.getModeText(eventItem))); + } + } + + if(event.getAction().isRightClick() && eventItem != null) { + if(eventItem.getType() == Material.FIREWORK_ROCKET) { + if(eventItem.getEnchantmentLevel(Enchantment.INFINITY) > 0) { + event.setUseInteractedBlock(Event.Result.ALLOW); + event.setUseItemInHand(Event.Result.DENY); + + if(player.isGliding()) { + player.fireworkBoost(eventItem); + } + else if(event.getAction() == Action.RIGHT_CLICK_BLOCK) { + if(interactionPoint != null && eventItem.getItemMeta() instanceof FireworkMeta fire) { + Firework work = interactionPoint.getWorld().spawn(interactionPoint, Firework.class); + work.setFireworkMeta(fire); + work.setShooter(player); + } + } + } + } + } + + if (event.getAction() == Action.LEFT_CLICK_BLOCK && block != null && block.getType() == Material.BARRIER) { + if (TomeAltarStorage.isTomeAltar(block.getLocation())) { + event.setCancelled(true); + ItemStack tomeItem = TomeAltarStorage.getItem(block.getLocation()); + BlazingGames.get().log(tomeItem); + if (tomeItem == null) tomeItem = new ItemStack(Material.AIR); + ItemStack finalTomeItem = tomeItem; + TomeAltarStorage.removeTomeAltar(block.getLocation()); + Location bLoc = block.getLocation().toCenterLocation(); + List entities = new ArrayList<>(); + for (Entity e : player.getWorld().getEntities()) { + Location eLoc = e.getLocation().toCenterLocation(); + if (eLoc.getX() == bLoc.getX() && eLoc.getY() == bLoc.getY() && eLoc.getZ() == bLoc.getZ()) { + entities.add(e); + } + } + for (Entity e : entities) { + e.remove(); + } + player.getWorld().setBlockData(bLoc, Material.AIR.createBlockData()); + Bukkit.getScheduler().runTaskLater(BlazingGames.get(), () -> InventoryUtils.collectableDrop(player, bLoc, CustomItems.TOME_ALTAR.create(), finalTomeItem), 1); + } + } + + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null && block.getType() == Material.BARRIER) { + if(!BlazingGames.get().interactCooldown.onCooldown(player)) { + if (TomeAltarStorage.isTomeAltar(block.getLocation())) { + BlazingGames.get().interactCooldown.setCooldown(player, 4); + + event.setCancelled(true); + + ItemStack giveItemNullable = TomeAltarStorage.getItem(block.getLocation()); + final ItemStack giveItem = giveItemNullable == null ? new ItemStack(Material.AIR) : giveItemNullable; + + ItemStack item = event.getItem(); + if (item == null) item = new ItemStack(Material.AIR); + + if(!giveItem.isSimilar(item)) { + ItemStack setItem = item.clone(); + item.setAmount(item.getAmount() - 1); + + setItem.setAmount(1); + TomeAltarStorage.setItem(block.getLocation(), setItem); + + Location bLoc = block.getLocation().toCenterLocation(); + ItemDisplay display = null; + for (Entity e : player.getWorld().getEntities()) { + Location eLoc = e.getLocation().toCenterLocation(); + if (eLoc.getX() == bLoc.getX() && eLoc.getY() == bLoc.getY() && eLoc.getZ() == bLoc.getZ() && e.getType() == EntityType.ITEM_DISPLAY) { + display = (ItemDisplay) e; + break; + } + } + if (display == null) { + Location loc = block.getLocation().toCenterLocation(); + loc.setY(loc.getY() + 0.375); + loc.setX(loc.getX()); + loc.setZ(loc.getZ()); + + display = (ItemDisplay) block.getWorld().spawnEntity(loc, EntityType.ITEM_DISPLAY); + display.setItemStack(setItem); + Transformation transformation = new Transformation(new Vector3f(), new Quaternionf(),new Vector3f(0.25f, 0.25f, 0.25f),new Quaternionf()); + display.setTransformation(transformation); + } else { + display.setItemStack(setItem); + } + + Bukkit.getScheduler().runTaskLater(BlazingGames.get(), () -> { + if (!player.getInventory().addItem(giveItem).values().isEmpty()) { + player.getWorld().dropItem(player.getLocation(), giveItem); + } + }, 1); + } + } + } + } + + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && block != null && hand != null) { + Vector v = face.getDirection(); + Location nextBlock = block.getLocation().add(v).toCenterLocation(); + Collection shulkers = nextBlock.getNearbyEntitiesByType(Shulker.class, 0.5); + if (!shulkers.isEmpty() && shulkers.size() < 8) { + Shulker shulker = shulkers.iterator().next(); + double y = shulker.getY(); + if (BlockPlaceEventListener.isTop(y)) nextBlock.add(0, -0.5, 0); + PersistentDataContainer container = shulker.getPersistentDataContainer(); + + ItemStack item = event.getPlayer().getInventory().getItem(hand); + boolean isSlab = CustomItem.getCustomItem(item) != null && Objects.requireNonNull(item.getPersistentDataContainer().get(BlazingGames.get().key("custom_item"), PersistentDataType.STRING)).contains("slab"); + if (isSlab && container.has(BlazingGames.get().key("slab")) && container.has(BlazingGames.get().key("slab_type"))) { + CustomSlabs.CustomSlab slab = (CustomSlabs.CustomSlab) CustomItem.getCustomItem(item); + if (event.getPlayer().getGameMode() != GameMode.CREATIVE) + event.getPlayer().getInventory().getItem(hand).setAmount(event.getPlayer().getInventory().getItem(hand).getAmount() - 1); + BlockPlaceEventListener.placeCustomSlab(slab, nextBlock, player.getEyeLocation(), face); + } + } + } + } + + private void spawnerInteractions(Player player, EquipmentSlot hand, ItemStack mainHand, CreatureSpawner spawner) { + if(hand != EquipmentSlot.HAND) { + return; + } + + if(CustomItem.isCustomItem(mainHand)) { + return; + } + + boolean inverted = player.getInventory().getItemInOffHand().getType().equals(Material.QUARTZ); + + boolean successful = false; + + if (mainHand.getType() == Material.SUGAR && ( + (spawner.getMinSpawnDelay() - 5 > 0 && !inverted && spawner.getMinSpawnDelay()-5 <= spawner.getMaxSpawnDelay()) || + (spawner.getMinSpawnDelay() + 5 <= 1000 && inverted && spawner.getMinSpawnDelay()+5 <= spawner.getMaxSpawnDelay()) + )) { + if (!inverted) spawner.setMinSpawnDelay(spawner.getMinSpawnDelay() - 5); + else spawner.setMinSpawnDelay(spawner.getMinSpawnDelay() + 5); + successful = true; + } + if (mainHand.getType() == Material.CLOCK && ( + (spawner.getMaxSpawnDelay() - 5 > 0 && !inverted && spawner.getMinSpawnDelay() <= spawner.getMaxSpawnDelay()-5) || + (spawner.getMaxSpawnDelay() + 5 <= 1000 && inverted && spawner.getMinSpawnDelay() <= spawner.getMaxSpawnDelay()+5) + )) { + if (!inverted) spawner.setMaxSpawnDelay(spawner.getMaxSpawnDelay() - 5); + else spawner.setMaxSpawnDelay(spawner.getMaxSpawnDelay() + 5); + successful = true; + } + if (mainHand.getType() == Material.FERMENTED_SPIDER_EYE && ( + (spawner.getSpawnCount() + 1 <= 20 && !inverted) || + (spawner.getSpawnCount() - 1 > 0 && inverted) + )) { + if (!inverted) spawner.setSpawnCount(spawner.getSpawnCount() + 1); + else spawner.setSpawnCount(spawner.getSpawnCount() - 1); + successful = true; + } + if (mainHand.getType() == Material.GHAST_TEAR && ( + (spawner.getMaxNearbyEntities() + 2 <= 200 && !inverted) || + (spawner.getMaxNearbyEntities() - 2 > 0 && inverted && spawner.getMaxNearbyEntities() != 32767) + )) { + if (!inverted) spawner.setMaxNearbyEntities(spawner.getMaxNearbyEntities() + 2); + else spawner.setMaxNearbyEntities(spawner.getMaxNearbyEntities() - 2); + successful = true; + } + if (mainHand.getType() == Material.PRISMARINE_CRYSTALS && ( + (spawner.getRequiredPlayerRange() + 2 <= 50 && !inverted) || + (spawner.getRequiredPlayerRange() - 2 > 0 && inverted && spawner.getRequiredPlayerRange() != 32767) + )) { + if (!inverted) spawner.setRequiredPlayerRange(spawner.getRequiredPlayerRange() + 2); + else spawner.setRequiredPlayerRange(spawner.getRequiredPlayerRange() - 2); + successful = true; + } + if (mainHand.getType() == Material.BLAZE_ROD && ( + (spawner.getSpawnRange() + 1 <= 20 && !inverted) || + (spawner.getSpawnRange() - 1 > 0 && inverted) + )) { + if (!inverted) spawner.setSpawnRange(spawner.getSpawnRange() + 1); + else spawner.setSpawnRange(spawner.getSpawnRange() - 1); + successful = true; + } + if (mainHand.getType() == Material.NETHER_STAR && ( + (spawner.getRequiredPlayerRange() != 32767 && !inverted) || + (spawner.getRequiredPlayerRange() == 32767 && inverted) + )) { + if (!inverted) spawner.setRequiredPlayerRange(32767); + else spawner.setRequiredPlayerRange(16); + successful = true; + } + if (mainHand.getType() == Material.CHORUS_FRUIT && ( + (spawner.getMaxNearbyEntities() != 32767 && !inverted) || + (spawner.getMaxNearbyEntities() == 32767 && inverted) + )) { + if (!inverted) spawner.setMaxNearbyEntities(32767); + else spawner.setMaxNearbyEntities(6); + successful = true; + } + PersistentDataContainer dataContainer = spawner.getPersistentDataContainer(); + if (mainHand.getType() == Material.COMPARATOR && + inverted == dataContainer.getOrDefault(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, false) + ) { + if (!inverted) dataContainer.set(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, true); + else dataContainer.remove(BlazingGames.get().key("redstone_control")); + successful = true; + } + + if(successful) { + if (player.getGameMode() == GameMode.SURVIVAL || player.getGameMode() == GameMode.ADVENTURE) { + mainHand.subtract(); + player.getInventory().setItem(hand, mainHand); + } + } + + spawner.update(); + } + + @SuppressWarnings("unchecked") + private void vaultShit(Block block) { + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> { + if (!(block.getState() instanceof CraftVault vault)) return; + VaultBlockEntity vaultBlockEntity = vault.getTileEntity(); + VaultServerData vaultServerData = vaultBlockEntity.getServerData(); + Method getItemsToEject; + try { + getItemsToEject = VaultServerData.class.getDeclaredMethod("getItemsToEject"); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + getItemsToEject.setAccessible(true); + List items; + try { + items = (List) getItemsToEject.invoke(vaultServerData); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + AtomicInteger i = new AtomicInteger(); + List finalItems = new ArrayList<>(items); + items.forEach(itemStack -> { + ItemStack bukkitItemStack = CraftItemStack.asBukkitCopy(itemStack); + if (bukkitItemStack.getType() == Material.ENCHANTED_BOOK) { + EnchantmentStorageMeta esm = (EnchantmentStorageMeta) bukkitItemStack.getItemMeta(); + if (esm.hasStoredEnchant(Enchantment.WIND_BURST)) { + finalItems.set(i.get(), CraftItemStack.asNMSCopy(CustomItems.STORM_TOME.create())); + } + } + i.addAndGet(1); + }); + try { + Method setItemsToEject = VaultServerData.class.getDeclaredMethod("setItemsToEject", List.class); + setItemsToEject.setAccessible(true); + setItemsToEject.invoke(vaultServerData, finalItems); + } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/InventoryCloseEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/InventoryCloseEventListener.java new file mode 100644 index 0000000..aff64b5 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/InventoryCloseEventListener.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.userinterfaces.UserInterface; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.InventoryCloseEvent; + +public class InventoryCloseEventListener implements Listener { + @EventHandler + public void onClose(InventoryCloseEvent event) { + if(event.getInventory().getHolder() instanceof UserInterface ui) { + if(event.getPlayer() instanceof Player p) { + ui.onClose(p); + } + } + } +} + + diff --git a/src/main/java/de/blazemcworld/blazinggames/events/InventoryDragEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/InventoryDragEventListener.java new file mode 100644 index 0000000..44ad264 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/InventoryDragEventListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.userinterfaces.UserInterface; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.*; + +public class InventoryDragEventListener implements Listener { + @EventHandler + public void onDrag(InventoryDragEvent event) { + if(event.getInventory().getHolder() instanceof UserInterface ui) { + ui.onDrag(event); + } + } +} + + diff --git a/src/main/java/de/blazemcworld/blazinggames/events/JoinEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/JoinEventListener.java new file mode 100644 index 0000000..1992f0b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/JoinEventListener.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.discord.DiscordApp; +import de.blazemcworld.blazinggames.discord.DiscordNotification; +import de.blazemcworld.blazinggames.items.CustomRecipes; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; + +public class JoinEventListener implements Listener { + @EventHandler + public void join(PlayerJoinEvent event) { + event.getPlayer().discoverRecipes(CustomRecipes.getAllRecipes().keySet()); + DiscordApp.send(DiscordNotification.playerJoin(event.getPlayer())); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/LootGenerateEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/LootGenerateEventListener.java new file mode 100644 index 0000000..e49f368 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/LootGenerateEventListener.java @@ -0,0 +1,154 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.items.CustomItems; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.world.LootGenerateEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; +import org.bukkit.loot.LootTables; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class LootGenerateEventListener implements Listener { + + @EventHandler + public void onLootGenerate(LootGenerateEvent event) { + List loot = event.getLoot(); + ArrayList newLoot = new ArrayList<>(loot); + + NamespacedKey key = event.getLootTable().getKey(); + + Random random = new Random(); + + if (key.equals(LootTables.BASTION_BRIDGE.getKey()) + || key.equals(LootTables.BASTION_OTHER.getKey()) + || key.equals(LootTables.BASTION_TREASURE.getKey())) { + int getBook = random.nextInt(100) + 1; + if (getBook <= 25) { + newLoot.add(CustomItems.DIM_TOME.create()); + } + } + if (key.equals(LootTables.PILLAGER_OUTPOST.getKey())) { + int getBook = random.nextInt(100) + 1; + if (getBook <= 50) { + newLoot.add(CustomItems.BLACK_TOME.create()); + } + } + if(key.equals(LootTables.PILLAGER_OUTPOST.getKey()) + || key.equals(LootTables.WOODLAND_MANSION.getKey())) { + int getBook = random.nextInt(100) + 1; + if (getBook <= 25) { + newLoot.add(CustomItems.BIND_TOME.create()); + } + getBook = random.nextInt(100) + 1; + if (getBook <= 25) { + newLoot.add(CustomItems.VANISH_TOME.create()); + } + } + if (key.equals(LootTables.END_CITY_TREASURE.getKey())) { + int getBook = random.nextInt(100) + 1; + if (getBook <= 11) { + newLoot.add(CustomItems.GUST_TOME.create()); + } + } + if(key.equals(LootTables.STRONGHOLD_CORRIDOR.getKey()) + || key.equals(LootTables.STRONGHOLD_CROSSING.getKey()) + || key.equals(LootTables.STRONGHOLD_LIBRARY.getKey()) + || key.equals(LootTables.END_CITY_TREASURE.getKey())) { + int getBook = random.nextInt(100) + 1; + if (getBook <= 8) { + newLoot.add(CustomItems.FUSE_TOME.create()); + } + } + if (key.equals(LootTables.IGLOO_CHEST.getKey())) { + int getBook = random.nextInt(100) + 1; + if (getBook <= 40) { + newLoot.add(CustomItems.CHILL_TOME.create()); + } + } + + if(key.equals(LootTables.ANCIENT_CITY.getKey())) { + for (int j = 0; j < newLoot.size(); j++) { + if (hasStoredEnchantment(newLoot.get(j), Enchantment.SWIFT_SNEAK)) { + newLoot.set(j, CustomItems.ECHO_TOME.create()); + } + } + } + + if(key.equals(LootTables.TRIAL_CHAMBERS_REWARD_OMINOUS_RARE.getKey())) { + for (int j = 0; j < newLoot.size(); j++) { + if (hasStoredEnchantment(newLoot.get(j), Enchantment.WIND_BURST)) { + newLoot.set(j, CustomItems.STORM_TOME.create()); + } + } + } + + if (key.equals(LootTables.BASTION_OTHER.getKey())) { + for (int j = 0; j < newLoot.size(); j++) { + if (hasStoredEnchantment(newLoot.get(j), Enchantment.SOUL_SPEED)) { + newLoot.set(j, CustomItems.NETHER_TOME.create()); + } + } + } + + if (key.equals(LootTables.BASTION_BRIDGE.getKey()) + || key.equals(LootTables.BASTION_OTHER.getKey()) + || key.equals(LootTables.BASTION_TREASURE.getKey()) + || key.equals(LootTables.BASTION_HOGLIN_STABLE.getKey())) { + for (int j = 0; j < newLoot.size(); j++) { + if (EnchantmentHelper.canEnchantItem(newLoot.get(j)) && random.nextInt(20) == 1) { + newLoot.set(j, EnchantmentHelper.enchantTool(newLoot.get(j), CustomEnchantments.UNSHINY, 1)); + } + } + } + + while(newLoot.size() > 27) { + newLoot.removeLast(); + } + + event.setLoot(newLoot); + } + + private boolean hasStoredEnchantment(ItemStack book, Enchantment enchantment) { + if(book.getItemMeta() instanceof EnchantmentStorageMeta esm) { + return esm.hasStoredEnchant(enchantment); + } + return false; + } + + private ItemStack createRandomBook(CustomEnchantment enchantment, Random random) { + return enchantRandomTool(new ItemStack(Material.BOOK), enchantment, random); + } + + private ItemStack createBook(CustomEnchantment enchantment, int level) { + return EnchantmentHelper.enchantTool(new ItemStack(Material.BOOK), enchantment, level); + } + + private ItemStack enchantRandomTool(ItemStack stack, CustomEnchantment enchantment, Random random) { + return EnchantmentHelper.enchantTool(stack, enchantment, random.nextInt(enchantment.getMaxLevel()) + 1); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/PiglinBarterEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/PiglinBarterEventListener.java new file mode 100644 index 0000000..55bf26a --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/PiglinBarterEventListener.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.items.CustomItems; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.PiglinBarterEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; + +import java.util.List; + +public class PiglinBarterEventListener implements Listener { + + @EventHandler + public void onPiglinBarter(PiglinBarterEvent event) { + List loot = event.getOutcome(); + + for (int j = 0; j < loot.size(); j++) { + if (hasStoredEnchantment(loot.get(j), Enchantment.SOUL_SPEED)) { + loot.set(j, CustomItems.NETHER_TOME.create()); + } + } + } + + private boolean hasStoredEnchantment(ItemStack book, Enchantment enchantment) { + if(book.getItemMeta() instanceof EnchantmentStorageMeta esm) { + return esm.hasStoredEnchant(enchantment); + } + return false; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/PrepareAnvilEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/PrepareAnvilEventListener.java new file mode 100644 index 0000000..00de2d8 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/PrepareAnvilEventListener.java @@ -0,0 +1,87 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.items.CustomItem; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.PrepareAnvilEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; + +public class PrepareAnvilEventListener implements Listener { + @EventHandler + public void onAnvilPrepare(PrepareAnvilEvent event) { + if (event.getView().getRepairCost() > 10) { + event.getView().setRepairCost(10); + } + + ItemStack in = event.getInventory().getFirstItem(); + ItemStack enchantingItem = event.getInventory().getSecondItem(); + ItemStack result = event.getInventory().getResult(); + + if(in == null || in.isEmpty() || enchantingItem == null || enchantingItem.isEmpty()) { + return; + } + + if ((in.getType() == Material.BOOK || in.getType() == Material.ENCHANTED_BOOK) && + (enchantingItem.getType() == Material.BOOK || enchantingItem.getType() == Material.ENCHANTED_BOOK)) { + event.setResult(null); + return; + } + + if(in.getType() == Material.FIREWORK_ROCKET) { + if(enchantingItem.getType() == Material.ENCHANTED_BOOK) { + if(enchantingItem.hasItemMeta() && enchantingItem.getItemMeta() instanceof EnchantmentStorageMeta esm) { + if(esm.hasStoredEnchant(Enchantment.INFINITY)) { + result = in.clone(); + result.addUnsafeEnchantment(Enchantment.INFINITY, 1); + event.setResult(result); + return; + } + } + } + } + + if(in.getAmount() != 1 || enchantingItem.getAmount() != 1) { + return; + } + + if(result == null || result.isEmpty()) { + result = in.clone(); + } + + if(!EnchantmentHelper.canEnchantItem(result)) { + return; + } + + if(CustomItem.isCustomItem(enchantingItem) || enchantingItem.getType() != Material.ENCHANTED_BOOK + && enchantingItem.getType() != result.getType()) { + return; + } + + result = EnchantmentHelper.enchantFromItem(result, enchantingItem); + + if(result.equals(in)) { + return; + } + + event.setResult(result); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/PrepareGrindstoneEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/PrepareGrindstoneEventListener.java new file mode 100644 index 0000000..9aa0558 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/PrepareGrindstoneEventListener.java @@ -0,0 +1,142 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantment; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.items.CustomItem; +import de.blazemcworld.blazinggames.utils.Pair; +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.PrepareGrindstoneEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.EnchantmentStorageMeta; + +public class PrepareGrindstoneEventListener implements Listener { + @EventHandler + public void onGrindstonePrepare(PrepareGrindstoneEvent event) { + ItemStack up = event.getInventory().getUpperItem(); + ItemStack down = event.getInventory().getLowerItem(); + ItemStack result = event.getInventory().getResult(); + + result = grindstoneItem(up, down, result); + + if(result == null) { + return; + } + + event.setResult(result); + } + + private ItemStack scrub(ItemStack tool, ItemStack sponge) { + ItemStack result = tool.clone(); + + if(CustomItem.isCustomItem(result)) return null; + + if(result.getType() == Material.ENCHANTED_BOOK) { + int total = EnchantmentHelper.getCustomEnchantments(result).size(); + if(result.getItemMeta() instanceof EnchantmentStorageMeta meta) { + total += meta.getStoredEnchants().size(); + } + + if(total <= 1) { + return null; + } + } + + if(sponge.getType() == Material.SPONGE) { + Pair entry = EnchantmentHelper.getEnchantmentEntryByIndex(tool, sponge.getAmount()); + + if(entry != null) { + if(result.getItemMeta() instanceof EnchantmentStorageMeta meta) { + meta.removeStoredEnchant(entry.left); + result.setItemMeta(meta); + } + else { + result.removeEnchantment(entry.left); + } + } + } + if(sponge.getType() == Material.WET_SPONGE) { + Pair entry = EnchantmentHelper.getCustomEnchantmentEntryByIndex(tool, sponge.getAmount()); + + if(entry != null) { + result = EnchantmentHelper.removeCustomEnchantment(result, entry.left); + } + } + + BlazingGames.get().log(result); + + if(result.equals(tool)) { + return null; + } + + return result; + } + + public ItemStack grindstoneItem(ItemStack up, ItemStack down, ItemStack result) { + if(CustomItem.isCustomItem(up)) return ItemStack.empty(); + if(CustomItem.isCustomItem(down)) return ItemStack.empty(); + + if(up == null || up.isEmpty()) { + ItemStack swap = down; + down = up; + up = swap; + if(up == null || up.isEmpty()) { + return null; + } + } + else if(down != null && !down.isEmpty()) + { + // double + if(down.getType() == Material.SPONGE || down.getType() == Material.WET_SPONGE) { + if(EnchantmentHelper.canEnchantItem(up)) { + return scrub(up, down); + } + } + if(up.getType() == Material.SPONGE || up.getType() == Material.WET_SPONGE) { + if(EnchantmentHelper.canEnchantItem(down)) { + return scrub(down, up); + } + } + + if(up.getType() != down.getType()) { + return null; + } + } + + if(result == null || result.isEmpty()) { + result = up.clone(); + } + + if(!EnchantmentHelper.hasCustomEnchantments(result)) { + return null; + } + + result = EnchantmentHelper.removeCustomEnchantments(result); + + if(result.equals(up)) { + return null; + } + + return result; + } +} + + diff --git a/src/main/java/de/blazemcworld/blazinggames/events/QuitEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/QuitEventListener.java new file mode 100644 index 0000000..e6bf9cf --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/QuitEventListener.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.discord.DiscordApp; +import de.blazemcworld.blazinggames.discord.DiscordNotification; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; + +public class QuitEventListener implements Listener { + @EventHandler + public void join(PlayerQuitEvent event) { + DiscordApp.send(DiscordNotification.playerLeave(event.getPlayer())); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/SpawnerSpawnEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/SpawnerSpawnEventListener.java new file mode 100644 index 0000000..18d29d0 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/SpawnerSpawnEventListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.SpawnerSpawnEvent; +import org.bukkit.persistence.PersistentDataType; + +public class SpawnerSpawnEventListener implements Listener { + + @EventHandler + public void onSpawnerActivate(SpawnerSpawnEvent event) { + CreatureSpawner spawner = event.getSpawner(); + if(spawner == null) return; + + if(spawner.getPersistentDataContainer().getOrDefault(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, false)) { + if(!spawner.getBlock().isBlockIndirectlyPowered()) { + event.setCancelled(true); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/TickEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/TickEventListener.java new file mode 100644 index 0000000..911c3a5 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/TickEventListener.java @@ -0,0 +1,192 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.ComputerRegistry; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.userinterfaces.UserInterface; +import de.blazemcworld.blazinggames.utils.TextLocation; +import de.blazemcworld.blazinggames.utils.TomeAltarStorage; +import io.papermc.paper.scoreboard.numbers.NumberFormat; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.*; +import org.bukkit.block.Block; +import org.bukkit.block.CreatureSpawner; +import org.bukkit.block.data.type.Campfire; +import org.bukkit.entity.*; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.ItemStack; +import org.bukkit.persistence.PersistentDataType; +import org.bukkit.scheduler.BukkitTask; +import org.bukkit.scoreboard.*; +import org.bukkit.util.RayTraceResult; +import org.bukkit.util.Vector; +import java.util.ArrayList; +import java.util.Objects; + +public class TickEventListener { + + private static int stupidrotate = 0; + + public static void onTick(BukkitTask object) { + stupidrotate += 8; + + for(World world : Bukkit.getServer().getWorlds()) { + ArrayList altars = new ArrayList<>(TomeAltarStorage.getAll(world)); + for(Entity entity : world.getEntities()) { + // rotate the altars + if (entity instanceof ItemDisplay display) { + for (Location location : altars) { + if (TextLocation.serializeRounded(location).equals(TextLocation.serializeRounded(display.getLocation()))) { + display.setRotation(stupidrotate, 0); + } + } + } + + if(entity instanceof LivingEntity l) { + ItemStack chestplate = null; + + if(entity instanceof Player p) { + chestplate = p.getInventory().getChestplate(); + } + else { + EntityEquipment eq = l.getEquipment(); + if(eq != null) { + chestplate = eq.getChestplate(); + } + } + + int updraft = EnchantmentHelper.getActiveCustomEnchantmentLevel(chestplate, + CustomEnchantments.UPDRAFT); + if(updraft > 0) { + if(l.isGliding()) { + if(smokeCovered(l.getLocation())) { + Vector velocity = l.getVelocity(); + velocity.add(new Vector(0, 0.1*updraft*updraft, 0)); + l.setVelocity(velocity); + + l.getWorld().playSound(l, Sound.ENTITY_WITCH_THROW, SoundCategory.BLOCKS, 2f, 0.5f); + } + } + } + } + } + + for (Player p : world.getPlayers()) { + if(p.getOpenInventory().getTopInventory().getHolder() instanceof UserInterface ui) { + ui.tick(p); + } + + if (p.getTargetBlockExact(5) != null && Objects.requireNonNull(p.getTargetBlockExact(5)).getType() == Material.SPAWNER) { + CreatureSpawner spawner = (CreatureSpawner) Objects.requireNonNull(p.getTargetBlockExact(5)).getState(); + Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); + + Objective objective = scoreboard.registerNewObjective("Spawner", Criteria.DUMMY, Component.text("Spawner").color(NamedTextColor.AQUA).decorate(TextDecoration.BOLD)); + objective.setDisplaySlot(DisplaySlot.SIDEBAR); + + Score minDelay = objective.getScore("§eMin Delay"); + minDelay.setScore(0); + minDelay.numberFormat(NumberFormat.fixed(Component.text(spawner.getMinSpawnDelay()).color(NamedTextColor.GREEN))); + + Score maxDelay = objective.getScore("§eMax Delay"); + maxDelay.setScore(-1); + maxDelay.numberFormat(NumberFormat.fixed(Component.text(spawner.getMaxSpawnDelay()).color(NamedTextColor.GREEN))); + + Score spawnCount = objective.getScore("§eSpawn Count"); + spawnCount.setScore(-2); + spawnCount.numberFormat(NumberFormat.fixed(Component.text(spawner.getSpawnCount()).color(NamedTextColor.GREEN))); + + Score maxNearby = objective.getScore("§eMax Nearby"); + maxNearby.setScore(-3); + maxNearby.numberFormat(NumberFormat.fixed(Component.text(spawner.getMaxNearbyEntities()).color(NamedTextColor.GREEN))); + + Score playerRange = objective.getScore("§ePlayer Range"); + playerRange.setScore(-4); + playerRange.numberFormat(NumberFormat.fixed(Component.text(spawner.getRequiredPlayerRange()).color(NamedTextColor.GREEN))); + + Score spawnRange = objective.getScore("§eSpawn Range"); + spawnRange.setScore(-5); + spawnRange.numberFormat(NumberFormat.fixed(Component.text(spawner.getSpawnRange()).color(NamedTextColor.GREEN))); + + Score redstoneControl = objective.getScore("§eRedstone Control"); + redstoneControl.setScore(-6); + if (spawner.getPersistentDataContainer().getOrDefault(BlazingGames.get().key("redstone_control"), PersistentDataType.BOOLEAN, false)) { + redstoneControl.numberFormat(NumberFormat.fixed(Component.text("Enabled").color(NamedTextColor.GREEN))); + } else { + redstoneControl.numberFormat(NumberFormat.fixed(Component.text("Disabled").color(NamedTextColor.RED))); + } + + p.setScoreboard(scoreboard); + } + else { + Scoreboard scoreboard = Bukkit.getScoreboardManager().getNewScoreboard(); + p.setScoreboard(scoreboard); + } + } + } + + ComputerRegistry.tick(); + } + + public static boolean smokeCovered(Location location) { + Block campfire = null; + + Location search = location.clone(); + + for(int i = 0; i < 25; i++) { + if(search.getBlock().getType() == Material.CAMPFIRE || search.getBlock().getType() == Material.SOUL_CAMPFIRE) { + campfire = search.getBlock(); + break; + } + search.add(0, -1, 0); + } + + if(campfire == null) { + return false; + } + + if(!(campfire.getBlockData() instanceof Campfire camp)) { + return false; + } + + if(!camp.isLit()) { + return false; + } + + int height = 10; + + if(camp.isSignalFire()) { + height = 24; + } + + RayTraceResult result = campfire.getWorld().rayTrace( + campfire.getLocation().toCenterLocation(), new Vector(0,1,0), height, FluidCollisionMode.NEVER, true, + 0, (e) -> false + ); + + BlazingGames.get().log(result); + + double actualHeight = result == null ? height : result.getHitPosition().getY(); + + double startingHeight = campfire.getLocation().toCenterLocation().getY(); + + return location.getY() >= startingHeight && location.getY() <= startingHeight + actualHeight; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/events/VillagerAcquireTradeEventListener.java b/src/main/java/de/blazemcworld/blazinggames/events/VillagerAcquireTradeEventListener.java new file mode 100644 index 0000000..8716aa6 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/events/VillagerAcquireTradeEventListener.java @@ -0,0 +1,77 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.events; + +import de.blazemcworld.blazinggames.items.CustomItems; +import org.bukkit.Material; +import org.bukkit.entity.AbstractVillager; +import org.bukkit.entity.Villager; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.VillagerAcquireTradeEvent; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.MerchantRecipe; + +import java.util.List; +import java.util.Random; + +public class VillagerAcquireTradeEventListener implements Listener { + private final List bannerPatterns = List.of( + Material.CREEPER_BANNER_PATTERN, + Material.FLOWER_BANNER_PATTERN, + Material.SKULL_BANNER_PATTERN, + Material.MOJANG_BANNER_PATTERN + ); + + @EventHandler + public void onVillagerAcquireTrade(VillagerAcquireTradeEvent event) { + AbstractVillager av = event.getEntity(); + MerchantRecipe recipe = event.getRecipe(); + + Random random = new Random(); + + if(av instanceof Villager villager) { + if(recipe.getResult().getType() == Material.ENCHANTED_BOOK) { + if (villager.getProfession() == Villager.Profession.LIBRARIAN) { + event.setRecipe(switch(villager.getVillagerLevel()) { + case 2 -> createRecipe(new ItemStack(Material.GLOW_INK_SAC), new ItemStack(Material.EMERALD, 3), 12, 5, 0.05f); + case 3 -> createRecipe(randomBannerPattern(random), new ItemStack(Material.EMERALD, 5), 12, 10, 0.2f); + case 4 -> createRecipe(CustomItems.GREED_TOME.create(), new ItemStack(Material.EMERALD, random.nextInt(10,39)), 12, 15, 0.2f); + default -> createRecipe(new ItemStack(Material.INK_SAC, 5), new ItemStack(Material.EMERALD, 1), 12, 2, 0.05f); + }); + } + } + } + } + + private MerchantRecipe createRecipe(ItemStack result, ItemStack ingredient, int maxUses, int villagerExperience, float priceMultiplier) { + MerchantRecipe recipe = new MerchantRecipe(result, 0, maxUses, true, villagerExperience, priceMultiplier); + recipe.addIngredient(ingredient); + return recipe; + } + + private MerchantRecipe createRecipe(ItemStack result, ItemStack ingredientA, ItemStack ingredientB, int maxUses, int villagerExperience, float priceMultiplier) { + MerchantRecipe recipe = new MerchantRecipe(result, 0, maxUses, true, villagerExperience, priceMultiplier); + recipe.addIngredient(ingredientA); + recipe.addIngredient(ingredientB); + return recipe; + } + + private ItemStack randomBannerPattern(Random random) { + int index = random.nextInt(bannerPatterns.size()); + return new ItemStack(bannerPatterns.get(index)); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/ColorlessItemPredicate.java b/src/main/java/de/blazemcworld/blazinggames/items/ColorlessItemPredicate.java new file mode 100644 index 0000000..6636c7d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/ColorlessItemPredicate.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.utils.ItemUtils; +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public class ColorlessItemPredicate implements ItemPredicate { + private final Material material; + + public ColorlessItemPredicate(Material material) { + this.material = ItemUtils.getUncoloredType(material); + } + + @Override + public boolean matchItem(ItemStack stack) { + if(CustomItem.isCustomItem(stack)) { + return false; + } + + return ItemUtils.getUncoloredType(stack) == material; + } + + @Override + public Component getDescription() { + return Component.text(switch(material) { + case WHITE_WOOL -> "Any Wool"; + case WHITE_BED -> "Any Bed"; + default -> "Unknown Colorless Material!"; + }); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/CustomItem.java b/src/main/java/de/blazemcworld/blazinggames/items/CustomItem.java new file mode 100644 index 0000000..e7a1128 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/CustomItem.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.utils.NamespacedKeyDataType; +import net.kyori.adventure.text.Component; +import org.bukkit.Keyed; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; + +public abstract class CustomItem implements RecipeProvider, Keyed, ItemPredicate { + private static final NamespacedKey key = BlazingGames.get().key("custom_item"); + + // returns null if not a custom item + public static @Nullable CustomItem getCustomItem(ItemStack stack) { + if(stack == null || !stack.hasItemMeta()) { + return null; + } + + if(!stack.getItemMeta().getPersistentDataContainer().has(key, NamespacedKeyDataType.instance)) { + return null; + } + + try { + return CustomItems.getByKey(stack.getItemMeta().getPersistentDataContainer().get(key, NamespacedKeyDataType.instance)); + } + catch(Exception err) { + BlazingGames.get().log(err); + return null; + } + } + + public static boolean isCustomItem(ItemStack stack) { + if(stack == null) return false; + + if(!stack.hasItemMeta()) { + return false; + } + + return stack.getItemMeta().getPersistentDataContainer().has(key, NamespacedKeyDataType.instance); + } + + public abstract @NotNull NamespacedKey getKey(); + + public final @NotNull ItemStack create() { + ItemStack result = material(); + + ItemMeta meta = result.getItemMeta(); + meta.getPersistentDataContainer().set(key, NamespacedKeyDataType.instance, getKey()); + result.setItemMeta(meta); + + return modifyMaterial(result); + } + + @Override + public final boolean matchItem(ItemStack stack) { + CustomItem other = getCustomItem(stack); + + if(other == null) return false; + + return other.getKey().equals(getKey()); + } + + @Override + public final Component getDescription() { + ItemStack item = create(); + + Component name = Component.translatable(item.translationKey()); + ItemMeta reqMeta = item.getItemMeta(); + + if(reqMeta != null) { + if(reqMeta.hasItemName()) { + name = reqMeta.itemName(); + } + + if(reqMeta.hasDisplayName()) { + name = reqMeta.displayName(); + } + } + + return name; + } + + // DO NOT CALL THIS METHOD, instead call create() on the item's instance + // also there's no need to set the "custom_item" item tag because + // the create() method does it anyway + // if you want to call a function that requires a custom item stack, + // do it in modifyMaterial(ItemStack stack) + protected abstract @NotNull ItemStack material(); + protected @NotNull ItemStack modifyMaterial(ItemStack stack) { + return stack; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/CustomItems.java b/src/main/java/de/blazemcworld/blazinggames/items/CustomItems.java new file mode 100644 index 0000000..2d557ae --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/CustomItems.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.builderwand.BuilderWand; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentTome; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentWrappers; +import de.blazemcworld.blazinggames.multiblocks.Blueprint; +import org.bukkit.NamespacedKey; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Set; + +public class CustomItems { + public static final BuilderWand BUILDER_WAND = new BuilderWand(); + public static final PortableCraftingTable PORTABLE_CRAFTING_TABLE = new PortableCraftingTable(); + public static final TeleportAnchor TELEPORT_ANCHOR = new TeleportAnchor(); + public static final Blueprint BLUEPRINT = new Blueprint(); + public static final TomeAltar TOME_ALTAR = new TomeAltar(); + public static final List CUSTOM_SLABS = new CustomSlabs().slabs; + public static final SkeletonKey SKELETON_KEY = new SkeletonKey(); + public static final ToGoBoxItem TO_GO_BOX = new ToGoBoxItem(); + public static final EnchantmentTome FUSE_TOME = new EnchantmentTome(BlazingGames.get().key("fuse_tome"), "Fuse Tome", EnchantmentWrappers.MENDING); + public static final EnchantmentTome BIND_TOME = new EnchantmentTome(BlazingGames.get().key("bind_tome"), "Bind Tome", EnchantmentWrappers.BINDING_CURSE); + public static final EnchantmentTome VANISH_TOME = new EnchantmentTome(BlazingGames.get().key("vanish_tome"), "Vanish Tome", EnchantmentWrappers.VANISHING_CURSE); + public static final EnchantmentTome CHILL_TOME = new EnchantmentTome(BlazingGames.get().key("chill_tome"), "Chill Tome", EnchantmentWrappers.FROST_WALKER); + public static final EnchantmentTome NETHER_TOME = new EnchantmentTome(BlazingGames.get().key("nether_tome"), "Nether Tome", EnchantmentWrappers.SOUL_SPEED); + public static final EnchantmentTome ECHO_TOME = new EnchantmentTome(BlazingGames.get().key("echo_tome"), "Echo Tome", EnchantmentWrappers.SWIFT_SNEAK); + public static final EnchantmentTome STORM_TOME = new EnchantmentTome(BlazingGames.get().key("storm_tome"), "Storm Tome", EnchantmentWrappers.WIND_BURST); + public static final EnchantmentTome BLACK_TOME = new EnchantmentTome(BlazingGames.get().key("black_tome"), "Black Tome", CustomEnchantments.CAPTURING); + public static final EnchantmentTome GUST_TOME = new EnchantmentTome(BlazingGames.get().key("gust_tome"), "Gust Tome", CustomEnchantments.UPDRAFT); + public static final EnchantmentTome GREED_TOME = new EnchantmentTome(BlazingGames.get().key("greed_tome"), "Greed Tome", CustomEnchantments.SCAVENGER); + public static final EnchantmentTome DIM_TOME = new EnchantmentTome(BlazingGames.get().key("dim_tome"), "Dim Tome", CustomEnchantments.UNSHINY); + + public static Set list() { + Set set = new java.util.HashSet<>(Set.of( + BUILDER_WAND, + PORTABLE_CRAFTING_TABLE, + TELEPORT_ANCHOR, + BLUEPRINT, + TOME_ALTAR, + SKELETON_KEY, + TO_GO_BOX, + FUSE_TOME, + BIND_TOME, + VANISH_TOME, + CHILL_TOME, + NETHER_TOME, + ECHO_TOME, + STORM_TOME, + BLACK_TOME, + GUST_TOME, + GREED_TOME, + DIM_TOME + )); + set.addAll(CUSTOM_SLABS); + return set; + } + + public static @Nullable CustomItem getByKey(NamespacedKey key) { + for(CustomItem curr : list()) { + if(curr.getKey().equals(key)) { + return curr; + } + } + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/CustomRecipes.java b/src/main/java/de/blazemcworld/blazinggames/items/CustomRecipes.java new file mode 100644 index 0000000..424f326 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/CustomRecipes.java @@ -0,0 +1,76 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import com.google.common.collect.ImmutableSet; +import org.bukkit.Bukkit; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.Recipe; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class CustomRecipes implements RecipeProvider { + private static final Map registeredRecipes = new HashMap<>(); + + private static Set getRecipeProviders() { + ImmutableSet.Builder providers = new ImmutableSet.Builder<>(); + providers.addAll(CustomItems.list().stream().toList()); + + providers.add(new SlabsToBlockRecipes()); + providers.add(new CustomRecipes()); + + return providers.build(); + } + + public static Map getAllRecipes() { + Map recipes = new HashMap<>(); + + for(RecipeProvider provider : getRecipeProviders()) { + recipes.putAll(provider.getRecipes()); + } + + return recipes; + } + + public static void loadRecipes() { + Map recipes = getAllRecipes(); + + for(Map.Entry recipe : recipes.entrySet()) { + registeredRecipes.put(recipe.getKey(), recipe.getValue()); + try { + Bukkit.addRecipe(recipe.getValue()); + } catch (IllegalStateException err) { + Bukkit.removeRecipe(recipe.getKey()); + Bukkit.addRecipe(recipe.getValue()); + } + } + } + + public static void unloadRecipes() { + for(NamespacedKey recipe : registeredRecipes.keySet()) { + try { + Bukkit.removeRecipe(recipe); + } catch (IllegalStateException ignored) {} + } + registeredRecipes.clear(); + } + + public Map getRecipes() { + return Map.of(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/CustomSlabs.java b/src/main/java/de/blazemcworld/blazinggames/items/CustomSlabs.java new file mode 100644 index 0000000..a397f8b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/CustomSlabs.java @@ -0,0 +1,138 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class CustomSlabs { + private final List blockedMaterials = List.of( + Material.COMMAND_BLOCK, + Material.CHAIN_COMMAND_BLOCK, + Material.REPEATING_COMMAND_BLOCK, + Material.SPAWNER, + Material.TRIAL_SPAWNER, + Material.BARRIER, + Material.JIGSAW, + Material.BEDROCK, + Material.SUSPICIOUS_GRAVEL, + Material.SUSPICIOUS_SAND, + Material.REINFORCED_DEEPSLATE + ); + + public final ArrayList slabs = new ArrayList<>(); + + public CustomSlabs() { + List materialNames = Arrays.stream(Material.values()).map(Material::name).toList(); + for (Material material : Material.values()) { + if (!blockedMaterials.contains(material) && material.isBlock() && material.isItem() && !material.isInteractable() && material.isOccluding() && material.isCollidable() && material.isSolid() && !material.name().contains("_PLANKS") && !material.name().contains("_SLAB") && !materialNames.contains(material.name() + "_SLAB")) { + if (material.name().endsWith("S")) { + if (materialNames.contains(material.name().substring(0, material.name().length() - 1) + "_SLAB")) + continue; + } + slabs.add(new CustomSlab(material)); + } + } + } + + public static class CustomSlab extends CustomItem { + public final Material material; + public final String name; + public final String camelName; + + private CustomSlab(Material material) { + this.material = material; + name = material.name().toLowerCase(); + camelName = formatMaterialName(name); + } + + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key(name + "_slab"); + } + + @Override + protected @NotNull ItemStack material() { + ItemStack item = new ItemStack(material); + + ItemMeta meta = item.getItemMeta(); + meta.setEnchantmentGlintOverride(true); + meta.itemName(Component.text(camelName + " Slab").color(NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false)); + item.setItemMeta(meta); + + return item; + } + + public Map getRecipes() { + ItemStack item = create(); + item.setAmount(6); + ShapedRecipe recipe = new ShapedRecipe(getKey(), item); + recipe.shape( + " ", + " ", + "MMM" + ); + recipe.setIngredient('M', new ItemStack(material)); + + ShapedRecipe recipe2 = new ShapedRecipe(BlazingGames.get().key(name), new ItemStack(material)); + recipe2.shape( + " ", + " M", + " M" + ); + recipe2.setIngredient('M', create()); + + return Map.of( + getKey(), recipe, + BlazingGames.get().key(name), recipe2 + ); + } + + private String formatMaterialName(String materialName) { + // Split the string by underscores + String[] words = materialName.split("_"); + + // Use a StringBuilder to construct the final result + StringBuilder formattedName = new StringBuilder(); + + for (String word : words) { + // Capitalize the first letter and append the rest of the word + if (word.length() > 0) { + formattedName.append(Character.toUpperCase(word.charAt(0))); // Capitalize first letter + formattedName.append(word.substring(1).toLowerCase()); // Append the rest in lowercase + formattedName.append(" "); // Add a space after each word + } + } + + // Remove the trailing space and return the result + return formattedName.toString().trim(); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/EmptyItemPredicate.java b/src/main/java/de/blazemcworld/blazinggames/items/EmptyItemPredicate.java new file mode 100644 index 0000000..3f46aa8 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/EmptyItemPredicate.java @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import net.kyori.adventure.text.Component; +import org.bukkit.inventory.ItemStack; + +public class EmptyItemPredicate implements ItemPredicate { + private EmptyItemPredicate() {} + + public static EmptyItemPredicate instance = new EmptyItemPredicate(); + + @Override + public boolean matchItem(ItemStack stack) { + return stack.isEmpty(); + } + + @Override + public Component getDescription() { + return Component.text("Literally nothing lol."); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/ItemPredicate.java b/src/main/java/de/blazemcworld/blazinggames/items/ItemPredicate.java new file mode 100644 index 0000000..b2f35d1 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/ItemPredicate.java @@ -0,0 +1,24 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import net.kyori.adventure.text.Component; +import org.bukkit.inventory.ItemStack; + +public interface ItemPredicate { + boolean matchItem(ItemStack stack); + Component getDescription(); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/MaterialItemPredicate.java b/src/main/java/de/blazemcworld/blazinggames/items/MaterialItemPredicate.java new file mode 100644 index 0000000..9879f6f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/MaterialItemPredicate.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import net.kyori.adventure.text.Component; +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; + +public class MaterialItemPredicate implements ItemPredicate { + private final Material material; + + public MaterialItemPredicate(Material material) { + this.material = material; + } + + @Override + public boolean matchItem(ItemStack stack) { + if(CustomItem.isCustomItem(stack)) { + return false; + } + + return stack.getType() == material; + } + + @Override + public Component getDescription() { + return Component.translatable(material.translationKey()); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/PortableCraftingTable.java b/src/main/java/de/blazemcworld/blazinggames/items/PortableCraftingTable.java new file mode 100644 index 0000000..dbb57f8 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/PortableCraftingTable.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import com.destroystokyo.paper.profile.PlayerProfile; +import com.destroystokyo.paper.profile.ProfileProperty; +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapelessRecipe; +import org.bukkit.inventory.meta.SkullMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; +import java.util.UUID; + +public class PortableCraftingTable extends CustomItem { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("portable_crafting_table"); + } + + @Override + protected @NotNull ItemStack material() { + int[] array = {-2038345071, 1713324536, -1078486308, 1585568499}; + long mostSignificantBits = ((long) array[0] << 32) | (array[1] & 0xFFFFFFFFL); + long leastSignificantBits = ((long) array[2] << 32) | (array[3] & 0xFFFFFFFFL); + + ItemStack item = new ItemStack(Material.PLAYER_HEAD); + SkullMeta meta = (SkullMeta) item.getItemMeta(); + PlayerProfile profile = Bukkit.createProfile(new UUID(mostSignificantBits, leastSignificantBits), "PortableCrafting"); + profile.setProperty(new ProfileProperty("textures","eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmNkYzBmZWI3MDAxZTJjMTBmZDUwNjZlNTAxYjg3ZTNkNjQ3OTMwOTJiODVhNTBjODU2ZDk2MmY4YmU5MmM3OCJ9fX0=", "Lfgwh10EgQOoFIRUYJjsKIDdlcOeXWSDAXi6CESfnb3E4pgr9ddqznPvsuo5d6fDhrpKSE4YblWMe1jv6d2M0vEzu+fkhWmG/NvBjAzOCaQ6j0V1urP3cU41nLbJax7eqdf5NDMh9uoXsmVUjUjwRAk4EotdBae4nTXdDNo47sLh80In6WMa04IA9eXkWC+gpNLfPUFgCf9Gn2RTttDSvyPCB5p7rSSd+03vOswGB4U7F1ttYEm1ih8PFzQQm7BLk3RL+L7chlMQqLyrURsPrH7OSKTrBB+Wxx6Z7pZS0yc8/8oRzEb8I6QdrGi1TANpuC1dorGCK4p6j7Pq6UQmUz0OjawEB0kc30v+CTGRnoDyQhLoKRguomxb4R7pCPHCOptAvNNaoFewOWsWPWlBg2lzRel2icKnjzvYyu6PR3Tj9lhNLUsK7IjV7Jqq6obzHNG/v/8dQL13WQPio8Uctkt/hi6b6QgM++lCdf9DbpgLpYPhID9vmXPuOgpVzBlImfYx67NYFPSb4EykG8sWZ1xoh3+y4/dHz2XMY10Q5IEZrxDuctn0oHlKdxi/R23DhJ9M5FwPYnvJ1Ew17EP03wDs85qcI2m5FQC2RwUvAWqBiQ5Qm+wykuWv7DYaul1Q0rSumuSjEIpCf3RxAozOKQOHploee2ekxPpwpHJltMo=")); + + meta.setPlayerProfile(profile); + meta.itemName(Component.text("Portable Crafting Table").color(NamedTextColor.WHITE)); + item.setItemMeta(meta); + + return item; + } + + public Map getRecipes() { + ShapelessRecipe portableCraftingTableRecipe = new ShapelessRecipe(getKey(), create()); + portableCraftingTableRecipe.addIngredient(Material.STICK); + portableCraftingTableRecipe.addIngredient(Material.CRAFTING_TABLE); + + return Map.of( + getKey(), portableCraftingTableRecipe + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/PotionItemPredicate.java b/src/main/java/de/blazemcworld/blazinggames/items/PotionItemPredicate.java new file mode 100644 index 0000000..d2839c5 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/PotionItemPredicate.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import net.kyori.adventure.text.Component; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.PotionMeta; +import org.bukkit.potion.PotionEffectType; +import org.bukkit.potion.PotionType; + +public class PotionItemPredicate implements ItemPredicate { + private final PotionEffectType effectType; + + public PotionItemPredicate(PotionEffectType effectType) { + this.effectType = effectType; + } + + @Override + public boolean matchItem(ItemStack stack) { + if(CustomItem.isCustomItem(stack)) { + return false; + } + + if(stack.getItemMeta() instanceof PotionMeta meta) { + PotionType type = meta.getBasePotionType(); + if(type != null) { + if(type.getPotionEffects().stream().anyMatch((effect) -> effect.getType().equals(effectType))) + { + return true; + } + } + return meta.hasCustomEffect(effectType); + } + + return false; + } + + @Override + public Component getDescription() { + for(PotionType type : PotionType.values()) { + if(type.getPotionEffects().stream().anyMatch((effect) -> effect.getType().equals(effectType))) { + String name = type.getKey().getKey().replaceFirst("^(long|strong)_", ""); + + return Component.translatable("item.minecraft.potion.effect."+name); + } + } + + return Component.text("Potion of ").append(Component.translatable(effectType.translationKey())); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/RecipeHelper.java b/src/main/java/de/blazemcworld/blazinggames/items/RecipeHelper.java new file mode 100644 index 0000000..55ab765 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/RecipeHelper.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import org.bukkit.Bukkit; +import org.bukkit.inventory.*; + +import java.util.Iterator; + +public class RecipeHelper { + public static boolean canBeSmelted(ItemStack stack) { + return smeltItem(stack) != null; + } + + public static ItemStack smeltItem(ItemStack stack) { + Iterator iter = Bukkit.recipeIterator(); + + while(iter.hasNext()) { + Recipe recipe = iter.next(); + if(recipe instanceof FurnaceRecipe fur) { + RecipeChoice choice = fur.getInputChoice(); + + if(choice.test(stack)) { + ItemStack result = fur.getResult(); + result.setAmount(stack.getAmount()); + return result; + } + } + } + + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/RecipeProvider.java b/src/main/java/de/blazemcworld/blazinggames/items/RecipeProvider.java new file mode 100644 index 0000000..f418d90 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/RecipeProvider.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.Recipe; + +import java.util.Map; + +public interface RecipeProvider { + default Map getRecipes() { + return Map.of(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/SkeletonKey.java b/src/main/java/de/blazemcworld/blazinggames/items/SkeletonKey.java new file mode 100644 index 0000000..8778922 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/SkeletonKey.java @@ -0,0 +1,57 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import java.util.List; +import java.util.Map; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.meta.ItemMeta; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +public class SkeletonKey extends CustomItem { + @Override + public NamespacedKey getKey() { + return BlazingGames.get().key("skeleton_key"); + } + + @Override + protected ItemStack material() { + ItemStack item = new ItemStack(Material.SKELETON_SKULL); + ItemMeta meta = item.getItemMeta(); + meta.itemName(Component.text("Skeleton Key").color(NamedTextColor.DARK_PURPLE).decoration(TextDecoration.ITALIC, false)); + meta.lore(List.of(Component.text("Used to open any crate that you've lost the key to.").color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true))); + item.setItemMeta(meta); + return item; + } + + @Override + public Map getRecipes() { + ShapedRecipe recipe = new ShapedRecipe(getKey(), create()); + recipe.shape(" D ", " B ", " B "); + recipe.setIngredient('D', Material.DIAMOND); + recipe.setIngredient('B', Material.BONE); + return Map.of(getKey(), recipe); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/SlabsToBlockRecipes.java b/src/main/java/de/blazemcworld/blazinggames/items/SlabsToBlockRecipes.java new file mode 100644 index 0000000..1b79492 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/SlabsToBlockRecipes.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.BlazingGames; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapedRecipe; + +import java.util.HashMap; +import java.util.Map; + +public class SlabsToBlockRecipes implements RecipeProvider { + @Override + public Map getRecipes() { + Map recipes = new HashMap<>(); + for (Material mat : Material.values()) { + if (mat.name().endsWith("_SLAB")) { + String slabName = mat.name().substring(0, mat.name().length() - 5); + Material block; + try { + block = Material.valueOf(slabName); + } catch (IllegalArgumentException err) { + try { + block = Material.valueOf(slabName + "S"); + } catch (IllegalArgumentException err2) { + try { + block = Material.valueOf(slabName + "_PLANKS"); + } catch (IllegalArgumentException err3) { + continue; + } + } + } + + ShapedRecipe recipe = new ShapedRecipe(BlazingGames.get().key(slabName), new ItemStack(block)); + recipe.shape( + " ", + " M", + " M" + ); + recipe.setIngredient('M', new ItemStack(mat)); + recipes.put(BlazingGames.get().key(slabName), recipe); + } + } + return recipes; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/TeleportAnchor.java b/src/main/java/de/blazemcworld/blazinggames/items/TeleportAnchor.java new file mode 100644 index 0000000..08280c4 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/TeleportAnchor.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class TeleportAnchor extends CustomItem { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("teleport_anchor"); + } + + @Override + protected @NotNull ItemStack material() { + ItemStack item = new ItemStack(Material.COMPASS); + ItemMeta meta = item.getItemMeta(); + meta.setEnchantmentGlintOverride(true); + meta.itemName(Component.text("Teleport Anchor").color(NamedTextColor.WHITE)); + List lore = new ArrayList<>(); + lore.add(Component.text("Click to show discovered lodestones.").color(NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false)); + meta.lore(lore); + meta.setEnchantmentGlintOverride(true); + item.setItemMeta(meta); + return item; + } + + public Map getRecipes() { + ShapedRecipe teleportAnchorRecipe = new ShapedRecipe(getKey(), create()); + teleportAnchorRecipe.shape( + "EEE", + "ECE", + "EEE" + ); + teleportAnchorRecipe.setIngredient('C', Material.COMPASS); + teleportAnchorRecipe.setIngredient('E', Material.ENDER_PEARL); + + return Map.of( + getKey(), teleportAnchorRecipe + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/ToGoBoxItem.java b/src/main/java/de/blazemcworld/blazinggames/items/ToGoBoxItem.java new file mode 100644 index 0000000..3617754 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/ToGoBoxItem.java @@ -0,0 +1,79 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapedRecipe; +import org.bukkit.inventory.ShapelessRecipe; +import org.bukkit.inventory.meta.ItemMeta; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; + +public class ToGoBoxItem extends CustomItem { + @Override + public NamespacedKey getKey() { + return BlazingGames.get().key("to_go_box"); + } + + @Override + protected ItemStack material() { + ItemStack item = new ItemStack(Material.NETHERITE_SHOVEL); + ItemMeta meta = item.getItemMeta(); + + meta.displayName(Component.text("To-Go Box").color(NamedTextColor.GOLD) + .decoration(TextDecoration.ITALIC, false)); + + meta.lore(List.of( + Component.text("Right-click a crate to open it and store it inside of a bundle, for transportation.") + .color(NamedTextColor.GRAY).decoration(TextDecoration.ITALIC, true) + )); + + item.setItemMeta(meta); + return item; + } + + @Override + public Map getRecipes() { + ShapedRecipe recipe = new ShapedRecipe(getKey(), create()); + recipe.shape( + "DBD", + "DED", + " D " + ); + recipe.setIngredient('D', new ItemStack(Material.DIAMOND)); + recipe.setIngredient('E', new ItemStack(Material.ENDER_CHEST)); + recipe.setIngredient('B', new ItemStack(Material.BUNDLE)); + + ShapelessRecipe bundleRecipe = new ShapelessRecipe(BlazingGames.get().key("bundle"), new ItemStack(Material.BUNDLE)); + bundleRecipe.addIngredient(new ItemStack(Material.LEATHER)); + bundleRecipe.addIngredient(new ItemStack(Material.STRING)); + + var out = new HashMap(); + out.put(getKey(), recipe); + out.put(BlazingGames.get().key("bundle"), bundleRecipe); + return out; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/items/TomeAltar.java b/src/main/java/de/blazemcworld/blazinggames/items/TomeAltar.java new file mode 100644 index 0000000..f487d71 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/items/TomeAltar.java @@ -0,0 +1,59 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.items; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.Recipe; +import org.bukkit.inventory.ShapelessRecipe; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.Map; + +public class TomeAltar extends CustomItem { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("tome_altar"); + } + + @Override + protected @NotNull ItemStack material() { + ItemStack item = new ItemStack(Material.BLACKSTONE_WALL); + + ItemMeta meta = item.getItemMeta(); + meta.setEnchantmentGlintOverride(true); + meta.itemName(Component.text("Tome Altar").color(NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false)); + item.setItemMeta(meta); + + return item; + } + + public Map getRecipes() { + ShapelessRecipe portableCraftingTableRecipe = new ShapelessRecipe(getKey(), create()); + portableCraftingTableRecipe.addIngredient(Material.ITEM_FRAME); + portableCraftingTableRecipe.addIngredient(Material.BLACKSTONE); + + return Map.of( + getKey(), portableCraftingTableRecipe + ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/BisectedBlockPredicate.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/BisectedBlockPredicate.java new file mode 100644 index 0000000..fa66e4a --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/BisectedBlockPredicate.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import org.bukkit.block.BlockState; +import org.bukkit.block.data.Bisected; +import org.bukkit.block.data.BlockData; + +public enum BisectedBlockPredicate implements BlockPredicate { + TOP(Bisected.Half.TOP), + BOTTOM(Bisected.Half.BOTTOM); + + private final Bisected.Half half; + + BisectedBlockPredicate(Bisected.Half half) { + this.half = half; + } + + @Override + public boolean matchBlock(BlockState block, int direction) { + BlockData data = block.getBlockData(); + + if(data instanceof Bisected bi) { + return bi.getHalf() == half; + } + + return true; + } + + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/BlockPredicate.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/BlockPredicate.java new file mode 100644 index 0000000..d2fa08d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/BlockPredicate.java @@ -0,0 +1,22 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import org.bukkit.block.BlockState; + +public interface BlockPredicate { + boolean matchBlock(BlockState block, int direction); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/Blueprint.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/Blueprint.java new file mode 100644 index 0000000..56ac489 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/Blueprint.java @@ -0,0 +1,92 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.items.CustomItem; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.*; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; +import org.bukkit.inventory.*; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class Blueprint extends CustomItem { + @Override + public @NotNull NamespacedKey getKey() { + return BlazingGames.get().key("blueprint"); + } + + @Override + protected @NotNull ItemStack material() { + ItemStack wand = new ItemStack(Material.PAPER); + + ItemMeta meta = wand.getItemMeta(); + + meta.addEnchant(Enchantment.CHANNELING, 1, true); + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + + meta.itemName(Component.text("Blueprint").color(NamedTextColor.BLUE)); + + wand.setItemMeta(meta); + + return wand; + } + + public void outputMultiBlockProgress(Player player, Location location) { + List output = new ArrayList<>(); + + for(MultiBlockStructureMetadata data : MultiBlockStructures.list()) { + if(data.matchTarget(location)) { + output.addAll(data.getProgress(location)); + } + } + + if(output.isEmpty()) { + output.add(Component.text("No Multi-Block Structure Found!").color(NamedTextColor.RED)); + player.playSound(player, Sound.BLOCK_NOTE_BLOCK_DIDGERIDOO, SoundCategory.PLAYERS, 2, 1); + } + else { + player.playSound(player, Sound.BLOCK_NOTE_BLOCK_BIT, SoundCategory.PLAYERS, 2, 1); + } + + for(Component component : output) { + player.sendMessage(component); + } + } + + public Map getRecipes() { + ShapedRecipe wandRecipe = new ShapedRecipe(getKey(), create()); + wandRecipe.shape( + "PPP", + "PBP", + "PPP" + ); + wandRecipe.setIngredient('P', Material.PAPER); + wandRecipe.setIngredient('B', Material.BLUE_DYE); + + return Map.of( + getKey(), wandRecipe + ); + } +} + diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/ComplexBlockPredicate.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/ComplexBlockPredicate.java new file mode 100644 index 0000000..413ea4f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/ComplexBlockPredicate.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import de.blazemcworld.blazinggames.BlazingGames; +import org.bukkit.block.BlockState; + +import java.util.ArrayList; +import java.util.List; + +public class ComplexBlockPredicate implements BlockPredicate { + List predicates = new ArrayList<>(); + + public ComplexBlockPredicate add(BlockPredicate... predicate) { + predicates.addAll(List.of(predicate)); + return this; + } + + public ComplexBlockPredicate(BlockPredicate... predicate) { + add(predicate); + } + + public List getPredicates() { + return predicates; + } + + @Override + public boolean matchBlock(BlockState block, int direction) { + for(BlockPredicate predicate : predicates) { + if(!predicate.matchBlock(block, direction)) { + BlazingGames.get().debugLog("Complex Predicate Failed. " + predicate); + return false; + } + } + return true; + } + + @Override + public String toString() { + return predicates.toString(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/DirectionalBlockPredicate.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/DirectionalBlockPredicate.java new file mode 100644 index 0000000..7b7b180 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/DirectionalBlockPredicate.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import org.bukkit.block.BlockFace; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.Directional; + +public enum DirectionalBlockPredicate implements BlockPredicate { + WEST(-1, 0, 0), + EAST(1, 0, 0), + NORTH(0, 0, -1), + SOUTH(0, 0, 1); + + private final int forward, vertical, right; + + DirectionalBlockPredicate(int forward, int vertical, int right) { + this.forward = forward; + this.vertical = vertical; + this.right = right; + } + + @Override + public boolean matchBlock(BlockState block, int direction) { + BlockData data = block.getBlockData(); + + if(data instanceof Directional dir) { + BlockFace face = dir.getFacing(); + + int forward = face.getModX(); + int vertical = face.getModY(); + int right = face.getModZ(); + + for(int i = 0; i < direction; i++) { + int temp = right; + right = -forward; + forward = temp; + } + + if(forward != this.forward) return false; + if(vertical != this.vertical) return false; + return right == this.right; + } + + return true; + } + + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructure.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructure.java new file mode 100644 index 0000000..6062fec --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructure.java @@ -0,0 +1,125 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.block.BlockState; +import org.bukkit.util.Vector; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MultiBlockStructure implements MultiBlockStructureMetadatable { + Map predicates = new HashMap<>(); + + public MultiBlockStructure add(Vector vec, BlockPredicate predicate) { + if(!predicates.containsKey(vec)) { + predicates.put(vec, predicate); + } + return this; + } + + public MultiBlockStructure addArea(Vector vec1, Vector vec2, BlockPredicate predicate) { + int x1 = vec1.getBlockX(); + int x2 = vec2.getBlockX(); + int y1 = vec1.getBlockY(); + int y2 = vec2.getBlockY(); + int z1 = vec1.getBlockZ(); + int z2 = vec2.getBlockZ(); + + if(x1 > x2) { + int temp = x1; + x1 = x2; + x2 = temp; + } + if(y1 > y2) { + int temp = y1; + y1 = y2; + y2 = temp; + } + if(z1 > z2) { + int temp = z1; + z1 = z2; + z2 = temp; + } + + for(int x = x1; x <= x2; x++) { + for(int y = y1; y <= y2; y++) { + for(int z = z1; z <= z2; z++) { + add(new Vector(x, y, z), predicate); + } + } + } + + return this; + } + + @Override + public int match(Location location, int direction) { + boolean matched = true; + + for(Map.Entry entry : predicates.entrySet()) { + Vector vec = entry.getKey().clone().rotateAroundAxis(new Vector(0, 1, 0), direction * Math.PI/2); + + Location loc = location.toBlockLocation().add(vec).toBlockLocation(); + + BlockState state = loc.getBlock().getState(); + + if(!entry.getValue().matchBlock(state, direction)) { + BlazingGames.get().debugLog("Failed block predicate! (Direction: "+direction+") " + loc + "=" + entry.getValue()); + + matched = false; + break; + } + } + + if(matched) return 1; + + return 0; + } + + @Override + public boolean matchTarget(Location location, int direction) { + Vector target = new Vector(0,0,0); + + for(Map.Entry predicate : predicates.entrySet()) { + if(predicate.getKey().equals(target)) { + if(predicate.getValue().matchBlock(location.toBlockLocation().getBlock().getState(), direction)) { + return true; + } + } + } + + return false; + } + + @Override + public List getProgress(Location loc) { + boolean matched = match(loc.toBlockLocation()) > 0; + + Component component = Component.text(matched ? "VALID" : "INVALID") + .color(matched ? NamedTextColor.GREEN : NamedTextColor.RED); + + return List.of(component); + } + + public Map getPredicates() { return predicates; } +} + diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMatcher.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMatcher.java new file mode 100644 index 0000000..9cc20fd --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMatcher.java @@ -0,0 +1,44 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import net.kyori.adventure.text.Component; +import org.bukkit.Location; + +import java.util.List; + +public interface MultiBlockStructureMatcher { + default int match(Location location) { + int max = 0; + + for(int i = 0; i < 4; i++) { + int level = match(location, i); + + if(level > max) max = level; + } + + return max; + } + int match(Location location, int direction); + default boolean matchTarget(Location location) { + for(int i = 0; i < 4; i++) { + if(matchTarget(location, i)) return true; + } + return false; + } + boolean matchTarget(Location location, int direction); + List getProgress(Location loc); +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMetadata.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMetadata.java new file mode 100644 index 0000000..15b1c31 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMetadata.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextDecoration; +import org.bukkit.Keyed; +import org.bukkit.Location; +import org.bukkit.NamespacedKey; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class MultiBlockStructureMetadata implements MultiBlockStructureMatcher, Keyed { + private final MultiBlockStructureMetadatable structure; + private final NamespacedKey key; + private final String name; + private final Style style; + + public MultiBlockStructureMetadata(NamespacedKey key, String name, Style style, MultiBlockStructureMetadatable structure) { + this.structure = structure; + this.key = key; + this.name = name; + this.style = style; + } + + @Override + public List getProgress(Location loc) { + ArrayList components = new ArrayList<>(structure.getProgress(loc)); + + Component title = Component.text(name + ": ").style(style); + + if(components.isEmpty()) { + components.add(Component.text("No response...").color(NamedTextColor.DARK_GRAY).decorate(TextDecoration.ITALIC)); + } + + Component component = Component.join(JoinConfiguration.builder().build(), title, components.get(0)); + + components.set(0, component); + + return components; + } + + @Override + public int match(Location location) { + return structure.match(location); + } + + @Override + public int match(Location location, int direction) { + return structure.match(location, direction); + } + + @Override + public boolean matchTarget(Location location) { + return structure.matchTarget(location); + } + + @Override + public boolean matchTarget(Location location, int direction) { + return structure.matchTarget(location, direction); + } + + @Override + public @NotNull NamespacedKey getKey() { + return key; + } + + public MultiBlockStructureMetadatable getStructure() { + return structure; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMetadatable.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMetadatable.java new file mode 100644 index 0000000..776fe51 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructureMetadatable.java @@ -0,0 +1,19 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +public interface MultiBlockStructureMetadatable extends MultiBlockStructureMatcher { +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructures.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructures.java new file mode 100644 index 0000000..0801ede --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiBlockStructures.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import de.blazemcworld.blazinggames.enchantments.sys.altar.AltarOfEnchanting; +import org.bukkit.NamespacedKey; + +import javax.annotation.Nullable; +import java.util.Set; + +public class MultiBlockStructures { + public static Set list() { + return Set.of( + AltarOfEnchanting.altar + ); + } + + public static @Nullable MultiBlockStructureMetadata getByKey(NamespacedKey key) { + for(MultiBlockStructureMetadata curr : list()) { + if(curr.getKey().equals(key)) { + return curr; + } + } + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiLevelBlockStructure.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiLevelBlockStructure.java new file mode 100644 index 0000000..6e5d6c9 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/MultiLevelBlockStructure.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import de.blazemcworld.blazinggames.utils.NumberUtils; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Location; + +import java.util.ArrayList; +import java.util.List; + +public class MultiLevelBlockStructure implements MultiBlockStructureMetadatable { + List levels = new ArrayList<>(); + + public MultiLevelBlockStructure add(MultiBlockStructure... structure) { + levels.addAll(List.of(structure)); + return this; + } + + public MultiLevelBlockStructure(MultiBlockStructure... structure) { + add(structure); + } + + @Override + public int match(Location location, int direction) { + for(int i = 0; i < levels.size(); i++) { + if(levels.get(i).match(location, direction) <= 0) { + return i; + } + } + return levels.size(); + } + + @Override + public boolean matchTarget(Location location, int direction) { + for (MultiBlockStructure level : levels) { + if (level.matchTarget(location, direction)) { + return true; + } + } + return false; + } + + @Override + public List getProgress(Location loc) { + int matched = match(loc); + + String tier = NumberUtils.getRomanNumber(matched) + "/" + NumberUtils.getRomanNumber(levels.size()); + + TextColor color = NamedTextColor.GREEN; + if(matched == 0) { + color = NamedTextColor.RED; + } + if(matched < levels.size()) { + color = NamedTextColor.YELLOW; + } + + Component component = Component.text(tier) + .color(color); + + return List.of(component); + } + + public List getLevels() { + return levels; + } +} + diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/SingleBlockPredicate.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/SingleBlockPredicate.java new file mode 100644 index 0000000..ef9c084 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/SingleBlockPredicate.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import org.bukkit.Material; +import org.bukkit.block.BlockState; + +public class SingleBlockPredicate implements BlockPredicate { + Material blockMaterial; + + public SingleBlockPredicate(Material blockMaterial) { + this.blockMaterial = blockMaterial; + } + + public Material getMaterial() { + return blockMaterial; + } + + @Override + public boolean matchBlock(BlockState block, int direction) { + return block.getType() == blockMaterial; + } + + @Override + public String toString() { + return blockMaterial.toString(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/multiblocks/StairShapeBlockPredicate.java b/src/main/java/de/blazemcworld/blazinggames/multiblocks/StairShapeBlockPredicate.java new file mode 100644 index 0000000..c240740 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/multiblocks/StairShapeBlockPredicate.java @@ -0,0 +1,105 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.multiblocks; + +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; +import org.bukkit.block.data.type.Stairs; + +public enum StairShapeBlockPredicate implements BlockPredicate { + STRAIGHT_NORTH(true, false, false, false, false), + STRAIGHT_SOUTH(false, true, false, false, false), + STRAIGHT_EAST(false, false, true, false, false), + STRAIGHT_WEST(false, false, false, true, false), + INNER_NORTH_WEST(true, false, false, true, false), + INNER_NORTH_EAST(true, false, true, false, false), + INNER_SOUTH_WEST(false, true, false, true, false), + INNER_SOUTH_EAST(false, true, true, false, false), + OUTER_NORTH_WEST(true, false, false, true, true), + OUTER_NORTH_EAST(true, false, true, false, true), + OUTER_SOUTH_WEST(false, true, false, true, true), + OUTER_SOUTH_EAST(false, true, true, false, true); + + private final boolean north, south, east, west, outer; + + StairShapeBlockPredicate(boolean north, boolean south, boolean east, boolean west, boolean outer) { + this.north = north; + this.south = south; + this.east = east; + this.west = west; + this.outer = outer; + } + + @Override + public boolean matchBlock(BlockState block, int direction) { + BlockData data = block.getBlockData(); + + if(data instanceof Stairs stairs) { + boolean north = false; + boolean south = false; + boolean east = false; + boolean west = false; + boolean outer = stairs.getShape() == Stairs.Shape.OUTER_LEFT || stairs.getShape() == Stairs.Shape.OUTER_RIGHT; + + switch(stairs.getFacing()) { + case EAST -> east = true; + case SOUTH -> south = true; + case WEST -> west = true; + case NORTH -> north = true; + } + + switch(stairs.getShape()) { + case OUTER_LEFT, INNER_LEFT -> { + switch(stairs.getFacing()) { + case EAST -> north = true; + case SOUTH -> east = true; + case WEST -> south = true; + case NORTH -> west = true; + } + } + case OUTER_RIGHT, INNER_RIGHT -> { + switch(stairs.getFacing()) { + case EAST -> south = true; + case SOUTH -> west = true; + case WEST -> north = true; + case NORTH -> east = true; + } + } + } + + for(int i = 0; i < direction; i++) { + boolean temp = east; + east = north; + north = west; + west = south; + south = temp; + } + + if(this.east != east) return false; + if(this.west != west) return false; + if(this.north != north) return false; + if(this.south != south) return false; + return this.outer == outer; + } + + return true; + } + + @Override + public String toString() { + return name(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneInteractionEventListener.java b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneInteractionEventListener.java new file mode 100644 index 0000000..55b48bd --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneInteractionEventListener.java @@ -0,0 +1,126 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.teleportanchor; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.items.CustomItems; +import de.blazemcworld.blazinggames.utils.TextLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.core.BlockPos; +import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket; +import net.minecraft.network.protocol.game.ServerboundSignUpdatePacket; +import net.minecraft.world.level.block.Blocks; +import org.bukkit.*; +import org.bukkit.block.BlockState; +import org.bukkit.block.Sign; +import org.bukkit.block.sign.Side; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class LodestoneInteractionEventListener implements Listener { + @EventHandler + public void onInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.RIGHT_CLICK_AIR || event.getAction() == Action.RIGHT_CLICK_BLOCK) { + ItemStack item = event.getItem(); + if (CustomItems.TELEPORT_ANCHOR.matchItem(item)) { + event.setCancelled(true); + if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.getClickedBlock() != null && event.getClickedBlock().getType() == Material.LODESTONE) { + Location signLocation = new Location(event.getPlayer().getWorld(), event.getClickedBlock().getLocation().getX(), event.getClickedBlock().getLocation().getY(), event.getClickedBlock().getLocation().getZ()); + List signLines = new ArrayList<>(); + signLines.add(Component.text("Lodestone name").color(NamedTextColor.AQUA)); + signLines.add(Component.text("vvvvvvvvvvvvvvv")); + signLines.add(Component.text("")); + signLines.add(Component.text("^^^^^^^^^^^^^^^")); + sendSignPacket(event.getPlayer(), signLocation, signLines); + } else openTeleportAnchor(event.getPlayer()); + } + } + } + + public void sendSignPacket(Player player, Location location, List lines) { + Bukkit.getScheduler().runTaskLater(BlazingGames.get(), () -> { + PacketHandler.addPacketInjector(player); + + BlockState blockState = Material.OAK_SIGN.createBlockData().createBlockState(); + Sign sign = (Sign) blockState; + for (int i = 0; i < 4; i++) sign.getSide(Side.FRONT).line(i, lines.get(i)); + player.sendBlockChange(location, sign.getBlockData()); + player.sendSignChange(location, lines); + + ClientboundOpenSignEditorPacket openSignEditorPacket = new ClientboundOpenSignEditorPacket( + new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), true + ); + + ((CraftPlayer) player).getHandle().connection.send(openSignEditorPacket); + + PacketHandler.PACKET_HANDLERS.put(player.getUniqueId(), packetO -> { + if (!(packetO instanceof ServerboundSignUpdatePacket packet)) return false; + + BlockPos blockPos = new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()); + + ClientboundBlockUpdatePacket sent3 = new ClientboundBlockUpdatePacket(blockPos, Blocks.LODESTONE.defaultBlockState()); + ((CraftPlayer) player).getHandle().connection.send(sent3); + + LodestoneStorage.saveLodestoneToPlayer(player.getUniqueId(), location, packet.getLines()[2]); + player.playSound(player.getLocation(), Sound.BLOCK_ENCHANTMENT_TABLE_USE, 1, 1); + return true; + }); + }, 1); + } + + public static void openTeleportAnchor(Player player) { + Inventory inventory = Bukkit.createInventory(null, 54, Component.text("Teleportation Menu").color(NamedTextColor.AQUA)); + Map lodestones = LodestoneStorage.getSavedLodestones(player.getUniqueId()); + ItemStack[] items = new ItemStack[lodestones.size()]; + + int index = 0; + for (Location lodestoneLoc : lodestones.keySet()) { + String lodestoneName = lodestones.get(lodestoneLoc); + ItemStack lodeStoneItem = new ItemStack(Material.LODESTONE); + + ItemMeta meta = lodeStoneItem.getItemMeta(); + meta.displayName(Component.text(lodestoneName).color(NamedTextColor.AQUA).decoration(TextDecoration.ITALIC, false)); + + List lore = new ArrayList<>(); + lore.add(Component.text("World: " + lodestoneLoc.getWorld().getName()).color(NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false)); + lore.add(Component.text("X: " + lodestoneLoc.getBlockX() + " Y: " + lodestoneLoc.getBlockY() + " Z: " + lodestoneLoc.getBlockZ()).color(NamedTextColor.WHITE).decoration(TextDecoration.ITALIC, false)); + meta.lore(lore); + + meta.getPersistentDataContainer().set(new NamespacedKey("blazinggames", "loc"), PersistentDataType.STRING, TextLocation.serializeRounded(lodestoneLoc)); + + lodeStoneItem.setItemMeta(meta); + items[index] = lodeStoneItem; + index++; + } + inventory.setContents(items); + player.openInventory(inventory); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneInventoryClickEventListener.java b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneInventoryClickEventListener.java new file mode 100644 index 0000000..3d4815f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneInventoryClickEventListener.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.teleportanchor; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.meta.ItemMeta; +import org.bukkit.persistence.PersistentDataType; + +import de.blazemcworld.blazinggames.utils.TextLocation; + +public class LodestoneInventoryClickEventListener implements Listener { + + @EventHandler + public void onLodestoneClick(InventoryClickEvent event) { + if (event.getView().title().equals(Component.text("Teleportation Menu").color(NamedTextColor.AQUA)) && event.getCurrentItem() != null) { + event.setCancelled(true); + Player player = (Player) event.getWhoClicked(); + ItemMeta meta = event.getCurrentItem().getItemMeta(); + + if (!meta.getPersistentDataContainer().has(new NamespacedKey("blazinggames", "loc"))) return; + Location location = TextLocation.deserialize(meta.getPersistentDataContainer().get(new NamespacedKey("blazinggames", "loc"), PersistentDataType.STRING)); + if (location == null) return; + + if (location.getBlock().getType() != Material.LODESTONE || event.getClick() == ClickType.SHIFT_LEFT || event.getClick() == ClickType.SHIFT_RIGHT) { + LodestoneStorage.removeSavedLodestoneForPlayer(player.getUniqueId(), location); + player.playSound(player.getLocation(), Sound.ENTITY_ENDER_EYE_DEATH, 1, 1); + LodestoneInteractionEventListener.openTeleportAnchor(player); + } else { + location = location.toCenterLocation(); + location.setY(location.getY() + 1); + location.setPitch(player.getLocation().getPitch()); + location.setYaw(player.getLocation().getYaw()); + player.teleport(location); + player.playSound(location, Sound.ITEM_CHORUS_FRUIT_TELEPORT, 1, 1); + event.getView().close(); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneStorage.java b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneStorage.java new file mode 100644 index 0000000..10c3952 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/LodestoneStorage.java @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.teleportanchor; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import com.google.common.reflect.TypeToken; + +import de.blazemcworld.blazinggames.data.DataStorage; +import de.blazemcworld.blazinggames.data.compression.GZipCompressionProvider; +import de.blazemcworld.blazinggames.data.providers.ULIDNameProvider; +import de.blazemcworld.blazinggames.data.storage.GsonStorageProvider; +import de.blazemcworld.blazinggames.utils.TextLocation; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; + +public class LodestoneStorage { + private LodestoneStorage() {} + private static final DataStorage, String> dataStorage = DataStorage.forClass( + LodestoneStorage.class, null, + new GsonStorageProvider>(new TypeToken>() {}.getType()), + new ULIDNameProvider(), new GZipCompressionProvider() + ); + + public static void saveLodestoneToPlayer(UUID player, Location location, String customName) { + var storage = dataStorage.getData(TextLocation.serializeRounded(location)); + if (storage == null) storage = new HashMap<>(); + storage.put(player, customName); + dataStorage.storeData(TextLocation.serializeRounded(location), storage); + } + + public static void removeSavedLodestoneForPlayer(UUID player, Location location) { + var storage = dataStorage.getData(TextLocation.serializeRounded(location)); + if (storage == null) return; + storage.remove(player); + dataStorage.storeData(TextLocation.serializeRounded(location), storage); + } + + public static Map getSavedLodestones(UUID player) { + return dataStorage.query(storage -> storage.getOrDefault(player, null) != null) + .stream().collect(Collectors.toMap(TextLocation::deserialize, i -> dataStorage.getData(i).getOrDefault(player, "error"))); + } + + public static void destoryLodestone(Location location) { + dataStorage.deleteData(TextLocation.serializeRounded(location)); + } + + public static void refreshAllInventories() { + for (Player p : Bukkit.getOnlinePlayers()) { + if (p.getOpenInventory().title().equals(Component.text("Teleportation Menu").color(NamedTextColor.AQUA))) { + LodestoneInteractionEventListener.openTeleportAnchor(p); + } + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/teleportanchor/PacketHandler.java b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/PacketHandler.java new file mode 100644 index 0000000..6d3893b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/teleportanchor/PacketHandler.java @@ -0,0 +1,74 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.teleportanchor; + +import de.blazemcworld.blazinggames.BlazingGames; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import net.minecraft.network.protocol.Packet; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.scheduler.BukkitRunnable; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import java.util.function.Predicate; + +public final class PacketHandler extends ChannelDuplexHandler { + + + public static final Map>> PACKET_HANDLERS = new HashMap<>(); + + private final Player p; // Store your target player + + public PacketHandler(Player p) { + this.p = p; + } + + private static final String PACKET_INJECTOR_ID = "blazinggames:packet_handler"; + + @Override + public void channelRead(@NotNull ChannelHandlerContext ctx, @NotNull Object packetO) throws Exception { + if (!(packetO instanceof Packet packet)) { // Utilize Java 17 features for pattern matching; Only intercept Packet Data + super.channelRead(ctx, packetO); + return; + } + + Predicate> handler = PACKET_HANDLERS.get(p.getUniqueId()); + if (handler != null) new BukkitRunnable() { + public void run() { + boolean success = handler.test(packet); // Check to make sure that the predicate works + if (success) PACKET_HANDLERS.remove(p.getUniqueId()); // If successful, remove the packet handler + } + }.runTask(BlazingGames.get()); // Execute your Predicate Handler on the Synchronous Thread + + super.channelRead(ctx, packetO); // Perform default actions done by the duplex handler + } + + public static void addPacketInjector(Player p) { + ServerPlayer sp = ((CraftPlayer) p).getHandle(); + + Channel ch = sp.connection.connection.channel; + + if (ch.pipeline().get(PACKET_INJECTOR_ID) != null) return; + ch.pipeline().addAfter("decoder", PACKET_INJECTOR_ID, new PacketHandler(p)); + } + +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/userinterfaces/InputSlot.java b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/InputSlot.java new file mode 100644 index 0000000..fe4d81f --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/InputSlot.java @@ -0,0 +1,151 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.userinterfaces; + +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +public class InputSlot extends UsableInterfaceSlot { + @Override + public final boolean onClick(UserInterface inventory, ItemStack current, ItemStack cursor, int slot, InventoryAction action, boolean isShiftClick, InventoryClickEvent event) { + return switch(action) { + case PICKUP_ALL, PICKUP_SOME, PICKUP_HALF, PICKUP_ONE, COLLECT_TO_CURSOR -> { + int pickupAmount = current.getAmount(); + + switch(action) { + case PICKUP_HALF -> pickupAmount /= 2; + case PICKUP_ONE -> pickupAmount = 1; + } + + if(cursor.isSimilar(current)) { + int available = cursor.getMaxStackSize() - cursor.getAmount(); + + if(available > pickupAmount) { + available = pickupAmount; + } + + cursor.add(available); + current.subtract(available); + } + else { + if(!isShiftClick) { + event.getWhoClicked().setItemOnCursor(current.clone()); + current.subtract(current.getAmount()); + } + } + + yield false; + } + case PLACE_ALL, PLACE_SOME, PLACE_ONE -> { + int placeAmount = cursor.getAmount(); + + if (action == InventoryAction.PLACE_ONE) { + placeAmount = 1; + } + + if(current.isEmpty() && filterItem(cursor)) { + int available = getDragMax(cursor); + if(available > placeAmount) { + available = placeAmount; + } + + inventory.setItem(slot, cursor.asQuantity(available)); + cursor.subtract(available); + } + else if(cursor.isSimilar(current)) { + int available = getDragMax(cursor) - current.getAmount(); + if(available > placeAmount) { + available = placeAmount; + } + + current.add(available); + cursor.subtract(available); + } + + yield false; + } + case SWAP_WITH_CURSOR -> { + if(cursor.getAmount() <= getDragMax(cursor) && filterItem(cursor)) { + event.getWhoClicked().setItemOnCursor(current); + inventory.setItem(slot, cursor); + } + + yield false; + } + case DROP_ALL_SLOT, DROP_ONE_SLOT, CLONE_STACK -> true; + case MOVE_TO_OTHER_INVENTORY -> { + if(isShiftClick) { + if(current.isEmpty() && filterItem(cursor)) { + int available = getDragMax(cursor); + if(available > cursor.getAmount()) { + available = cursor.getAmount(); + } + + inventory.setItem(slot, cursor.asQuantity(available)); + cursor.subtract(available); + } + else if(current.isSimilar(cursor)) { + int available = getDragMax(cursor) - current.getAmount(); + if(available > cursor.getAmount()) { + available = cursor.getAmount(); + } + + current.add(available); + cursor.subtract(available); + } + yield false; + } + yield true; + } + case HOTBAR_SWAP, HOTBAR_MOVE_AND_READD -> { + if(event.getHotbarButton() >= 0) { + ItemStack hotbar = event.getWhoClicked().getInventory().getItem(event.getHotbarButton()); + + if(hotbar == null) { + inventory.setItem(slot, ItemStack.empty()); + event.getWhoClicked().getInventory().setItem(event.getHotbarButton(), current); + } + else if(hotbar.getAmount() <= getDragMax(hotbar) && filterItem(hotbar)) { + inventory.setItem(slot, hotbar); + event.getWhoClicked().getInventory().setItem(event.getHotbarButton(), current); + } + } + else { + ItemStack hotbar = event.getWhoClicked().getInventory().getItemInOffHand(); + + if(hotbar.getAmount() <= getDragMax(hotbar) && filterItem(hotbar)) { + inventory.setItem(slot, event.getWhoClicked().getInventory().getItemInOffHand()); + event.getWhoClicked().getInventory().setItemInOffHand(current); + } + } + + yield false; + } + default -> false; + }; + } + + @Override + public int getDragMax(ItemStack value) { + if(value.isEmpty()) return 64; + return value.getMaxStackSize(); + } + + public boolean filterItem(ItemStack stack) { + return true; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/userinterfaces/SingleInputSlot.java b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/SingleInputSlot.java new file mode 100644 index 0000000..7db929c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/SingleInputSlot.java @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.userinterfaces; + +import org.bukkit.inventory.ItemStack; + +public class SingleInputSlot extends InputSlot { + @Override + public final int getDragMax(ItemStack value) { + return 1; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/userinterfaces/StaticUserInterfaceSlot.java b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/StaticUserInterfaceSlot.java new file mode 100644 index 0000000..8442372 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/StaticUserInterfaceSlot.java @@ -0,0 +1,31 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.userinterfaces; + +import org.bukkit.inventory.ItemStack; + +public class StaticUserInterfaceSlot implements UserInterfaceSlot { + private final ItemStack stack; + + public StaticUserInterfaceSlot(ItemStack stack) { + this.stack = stack; + } + + @Override + public void onUpdate(UserInterface inventory, int slot) { + inventory.setItem(slot, stack); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UsableInterfaceSlot.java b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UsableInterfaceSlot.java new file mode 100644 index 0000000..508808d --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UsableInterfaceSlot.java @@ -0,0 +1,30 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.userinterfaces; + +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +public abstract class UsableInterfaceSlot implements UserInterfaceSlot { + @Override + public final void onUpdate(UserInterface inventory, int slot) {} + + @Override + public boolean onClick(UserInterface inventory, ItemStack current, ItemStack cursor, int slot, InventoryAction action, boolean isShiftClick, InventoryClickEvent event) { + return true; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UserInterface.java b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UserInterface.java new file mode 100644 index 0000000..d93b24b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UserInterface.java @@ -0,0 +1,245 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.userinterfaces; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.utils.NamespacedKeyDataType; +import net.kyori.adventure.text.Component; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.NamespacedKey; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryDragEvent; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public abstract class UserInterface implements InventoryHolder { + public final static NamespacedKey guiKey = BlazingGames.get().key("gui"); + private final Inventory inventory; + protected final HashMap slots = new HashMap<>(); + private final int rows; + + public UserInterface(BlazingGames plugin, Component title, int rows) { + this.inventory = plugin.getServer().createInventory(this, rows*9, title); + this.rows = rows; + + preload(); + reload(); + } + + protected abstract void preload(); + + public UserInterface(BlazingGames plugin, String title, int rows) { + this(plugin, Component.text(title), rows); + } + + @Override + public final @NotNull Inventory getInventory() { + return this.inventory; + } + + protected void reload() { + for(Map.Entry slot : slots.entrySet()) { + slot.getValue().onUpdate(this, slot.getKey()); + } + } + + // returning false means that the event is cancelled, while true does the opposite + public boolean onClick(ItemStack clicked, ItemStack cursor, int slot, InventoryAction action, InventoryClickEvent event) { + boolean result = false; + + if(clicked == null) { + clicked = ItemStack.empty(); + } + + if(slots.containsKey(slot)) { + result = slots.get(slot).onClick(this, clicked, cursor, slot, action, false, event); + } + + Bukkit.getScheduler().runTask(BlazingGames.get(), this::reload); + + return result; + } + + public boolean onShiftClick(ItemStack clicked, InventoryAction action, InventoryClickEvent event) { + return switch(action) { + case MOVE_TO_OTHER_INVENTORY -> { + for(Map.Entry entry : slots.entrySet()) { + if(entry.getValue() instanceof UsableInterfaceSlot usable) { + if(usable.onClick(this, getItem(entry.getKey()), clicked, entry.getKey(), action, true, event)) { + break; + } + } + } + + Bukkit.getScheduler().runTask(BlazingGames.get(), this::reload); + + yield false; + } + case COLLECT_TO_CURSOR -> { + for(Map.Entry entry : slots.entrySet()) { + if(entry.getValue() instanceof UsableInterfaceSlot usable) { + if(usable.onClick(this, getItem(entry.getKey()), clicked, entry.getKey(), action, true, event)) { + break; + } + } + } + + Inventory playerInventory = event.getWhoClicked().getInventory(); + for (ItemStack stack : playerInventory) { + if (clicked.isSimilar(stack)) { + int amount = stack.getAmount(); + if (amount > clicked.getMaxStackSize() - clicked.getAmount()) { + amount = clicked.getMaxStackSize() - clicked.getAmount(); + } + clicked.add(amount); + stack.subtract(amount); + } + } + + Bukkit.getScheduler().runTask(BlazingGames.get(), this::reload); + + yield false; + } + default -> true; + }; + } + + public final void setItem(int slot, ItemStack stack) { + inventory.setItem(slot, stack); + } + + public final void setItem(int x, int y, ItemStack stack) { + setItem(x+y*9, stack); + } + + public final void setArea(int x1, int y1, int x2, int y2, ItemStack stack) { + if (x2 > x1) { + int temp = x2; + x2 = x1; + x1 = temp; + } + if (y2 > y1) { + int temp = y2; + y2 = y1; + y1 = temp; + } + + for (int j = y1; j <= y2; j++) { + for(int i = x1; i <= x2; i++) { + setItem(i, j, stack.clone()); + } + } + } + + public static ItemStack element(Material material, NamespacedKey elementKey) { + ItemStack element = new ItemStack(material); + + ItemMeta meta = element.getItemMeta(); + meta.getPersistentDataContainer().set(guiKey, NamespacedKeyDataType.instance, elementKey); + meta.setHideTooltip(true); + element.setItemMeta(meta); + + return element; + } + + public final ItemStack getItem(int slot) { + ItemStack result = inventory.getItem(slot); + return result == null ? ItemStack.empty() : result; + } + + public final ItemStack getItem(int x, int y) { + return getItem(x+y*9); + } + + protected final void addSlot(int x, int y, UserInterfaceSlot slot) { + if(x < 0 || x >= 9) { + throw new IllegalStateException("Can't have a slot outside bounds!"); + } + if(y < 0 || y >= rows) { + throw new IllegalStateException("Can't have a slot outside bounds!"); + } + slots.put(x+y*9, slot); + } + + public void onDrag(InventoryDragEvent event) { + event.setCancelled(true); + + int refund = 0; + + if(event.getCursor() != null) { + refund = event.getCursor().getAmount(); + } + + for(Map.Entry entry : event.getNewItems().entrySet()) { + Inventory dragInv = event.getWhoClicked().getOpenInventory().getInventory(entry.getKey()); + + ItemStack stack = entry.getValue(); + + if(dragInv != null) { + if(dragInv.getHolder() == this) + { + int max = 0; + + if(slots.containsKey(entry.getKey())) { + max = slots.get(entry.getKey()).getDragMax(entry.getValue()); + + if(slots.get(entry.getKey()) instanceof InputSlot i) { + if(!i.filterItem(entry.getValue())) { + max = 0; + } + } + } + + if(stack.getAmount() > max) { + int overfill = entry.getValue().getAmount() - max; + stack.subtract(overfill); + refund += overfill; + } + } + } + + event.getWhoClicked().getOpenInventory().setItem(entry.getKey(), stack); + } + + int finalRefund = refund; + Bukkit.getScheduler().runTask(BlazingGames.get(), () -> { + if(finalRefund > 0) { + event.getWhoClicked().setItemOnCursor(event.getOldCursor().asQuantity(finalRefund)); + } + else { + event.getWhoClicked().setItemOnCursor(null); + } + reload(); + }); + } + + public void tick(Player p) { + + } + + public void onClose(Player p) { + + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UserInterfaceSlot.java b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UserInterfaceSlot.java new file mode 100644 index 0000000..bc647d1 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/userinterfaces/UserInterfaceSlot.java @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.userinterfaces; + +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.inventory.ItemStack; + +public interface UserInterfaceSlot { + void onUpdate(UserInterface inventory, int slot); + + default boolean onClick(UserInterface inventory, ItemStack current, ItemStack cursor, int slot, InventoryAction action, boolean isShiftClick, InventoryClickEvent event) { + return false; + } + + default int getDragMax(ItemStack value) { + return 0; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/Box.java b/src/main/java/de/blazemcworld/blazinggames/utils/Box.java new file mode 100644 index 0000000..5bfbef0 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/Box.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import org.bukkit.block.BlockFace; +import org.bukkit.util.Vector; + +public class Box { + private final Face top, bottom, left, right, front, back; + + public Box(Face top, Face bottom, Face left, Face right, Face front, Face back) { + this.top = top; + this.bottom = bottom; + this.left = left; + this.right = right; + this.front = front; + this.back = back; + } + + public Vector getDirection(Vector v) { + if (top.contains(v)) return new Vector(0, 1, 0); + if (bottom.contains(v)) return new Vector(0, -1, 0); + if (left.contains(v)) return new Vector(-1, 0, 0); + if (right.contains(v)) return new Vector(1, 0, 0); + if (front.contains(v)) return new Vector(0, 0, 1); + if (back.contains(v)) return new Vector(0, 0, -1); + return null; + } + + public BlockFace getFace(Vector v) { + if (top.contains(v)) return BlockFace.UP; + if (bottom.contains(v)) return BlockFace.DOWN; + if (left.contains(v)) return BlockFace.WEST; + if (right.contains(v)) return BlockFace.EAST; + if (front.contains(v)) return BlockFace.NORTH; + if (back.contains(v)) return BlockFace.SOUTH; + return null; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/Cooldown.java b/src/main/java/de/blazemcworld/blazinggames/utils/Cooldown.java new file mode 100644 index 0000000..2dee1d8 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/Cooldown.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import java.util.HashMap; + +public class Cooldown { + private final HashMap cooldown = new HashMap<>(); + + public Cooldown(Plugin plugin) { + plugin.getServer().getScheduler().runTaskTimer(plugin, this::updateCooldowns, 1, 1); + } + + public void setCooldown(Player p, int ticks) { + cooldown.put(p, ticks); + } + + public void updateCooldowns() { + for(Player p : cooldown.keySet()) { + int ticks = cooldown.get(p); + ticks--; + if(ticks <= 0) { + cooldown.remove(p); + } + else { + cooldown.put(p, ticks); + } + } + } + + public boolean onCooldown(Player p) { + return cooldown.getOrDefault(p, 0) > 0; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/EnumDataType.java b/src/main/java/de/blazemcworld/blazinggames/utils/EnumDataType.java new file mode 100644 index 0000000..55dc99e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/EnumDataType.java @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +public class EnumDataType> implements PersistentDataType { + private final Class complexType; + + public EnumDataType(Class complexType) { + this.complexType = complexType; + } + @Override + public @NotNull Class getPrimitiveType() { + return String.class; + } + + @Override + public @NotNull Class getComplexType() { + return complexType; + } + + @Override + public @NotNull String toPrimitive(@NotNull T complex, @NotNull PersistentDataAdapterContext context) { + return complex.name().toLowerCase(); + } + + @Override + public @NotNull T fromPrimitive(@NotNull String primitive, @NotNull PersistentDataAdapterContext context) { + return T.valueOf(getComplexType(), primitive.toUpperCase()); + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/Face.java b/src/main/java/de/blazemcworld/blazinggames/utils/Face.java new file mode 100644 index 0000000..625a670 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/Face.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import org.bukkit.util.Vector; + +public class Face { + private final Vector v1, v2; + + public Face(Vector v1, Vector v2) { + this.v1 = v1; + this.v2 = v2; + } + + public boolean contains(Vector v) { + double minX = Math.min(v1.getX(), v2.getX()); + double maxX = Math.max(v1.getX(), v2.getX()); + double minY = Math.min(v1.getY(), v2.getY()); + double maxY = Math.max(v1.getY(), v2.getY()); + double minZ = Math.min(v1.getZ(), v2.getZ()); + double maxZ = Math.max(v1.getZ(), v2.getZ()); + + return (v.getX() >= minX && v.getX() <= maxX) && + (v.getY() >= minY && v.getY() <= maxY) && + (v.getZ() >= minZ && v.getZ() <= maxZ); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/GZipToolkit.java b/src/main/java/de/blazemcworld/blazinggames/utils/GZipToolkit.java new file mode 100644 index 0000000..39bf8ef --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/GZipToolkit.java @@ -0,0 +1,72 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import de.blazemcworld.blazinggames.BlazingGames; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +public class GZipToolkit { + public static byte[] compress(String input) { + return compressBytes(input.getBytes(StandardCharsets.UTF_8)); + } + + public static byte[] compressBytes(byte[] input) { + try { + byte[] bytes; + try ( + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + GZIPOutputStream gzipOut = new GZIPOutputStream(bytesOut); + ) { + gzipOut.write(input); + gzipOut.close(); + bytes = bytesOut.toByteArray(); + } + + return bytes; + } catch (IOException e) { + BlazingGames.get().log(e); + return null; + } + } + + public static String decompress(byte[] input) { + return new String(decompressBytes(input), StandardCharsets.UTF_8); + } + + public static byte[] decompressBytes(byte[] input) { + try { + byte[] data; + try ( + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + ByteArrayInputStream bytesIn = new ByteArrayInputStream(input); + GZIPInputStream gzipIn = new GZIPInputStream(bytesIn); + ) { + bytesOut.writeBytes(gzipIn.readAllBytes()); + data = bytesOut.toByteArray(); + } + + return data; + } catch (IOException e) { + BlazingGames.get().log(e); + return null; + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/GetGson.java b/src/main/java/de/blazemcworld/blazinggames/utils/GetGson.java new file mode 100644 index 0000000..40d7643 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/GetGson.java @@ -0,0 +1,133 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +public class GetGson { + private GetGson() { + } + + private static JsonPrimitive _getAsPrimitive(JsonElement element, T t) throws T { + if (element == null) { + throw t; + } else if (!element.isJsonPrimitive()) { + throw t; + } else { + return element.getAsJsonPrimitive(); + } + } + + public static String getString(JsonObject object, String key, T t) throws T { + if (object == null) { + throw t; + } else if (!object.has(key)) { + throw t; + } else { + return getAsString(object.get(key), t); + } + } + + public static String getAsString(JsonElement element, T t) throws T { + JsonPrimitive primitive = _getAsPrimitive(element, t); + if (!primitive.isString()) { + throw t; + } else { + return primitive.getAsString(); + } + } + + public static Number getNumber(JsonObject object, String key, T t) throws T { + if (object == null) { + throw t; + } else if (!object.has(key)) { + throw t; + } else { + return getAsNumber(object.get(key), t); + } + } + + public static Number getAsNumber(JsonElement element, T t) throws T { + JsonPrimitive primitive = _getAsPrimitive(element, t); + if (!primitive.isNumber()) { + throw t; + } else { + return primitive.getAsNumber(); + } + } + + public static Boolean getBoolean(JsonObject object, String key, T t) throws T { + if (object == null) { + throw t; + } else if (!object.has(key)) { + throw t; + } else { + return getAsBoolean(object.get(key), t); + } + } + + public static Boolean getAsBoolean(JsonElement element, T t) throws T { + JsonPrimitive primitive = _getAsPrimitive(element, t); + if (!primitive.isBoolean()) { + throw t; + } else { + return primitive.getAsBoolean(); + } + } + + public static JsonObject getObject(JsonObject object, String key, T t) throws T { + if (object == null) { + throw t; + } else if (!object.has(key)) { + throw t; + } else { + return getAsObject(object.get(key), t); + } + } + + public static JsonObject getAsObject(JsonElement element, T t) throws T { + if (element == null) { + throw t; + } else if (!element.isJsonObject()) { + throw t; + } else { + return element.getAsJsonObject(); + } + } + + public static JsonArray getArray(JsonObject object, String key, T t) throws T { + if (object == null) { + throw t; + } else if (!object.has(key)) { + throw t; + } else { + return getAsArray(object.get(key), t); + } + } + + public static JsonArray getAsArray(JsonElement element, T t) throws T { + if (element == null) { + throw t; + } else if (!element.isJsonArray()) { + throw t; + } else { + return element.getAsJsonArray(); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/InventoryUtils.java b/src/main/java/de/blazemcworld/blazinggames/utils/InventoryUtils.java new file mode 100644 index 0000000..a665a2b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/InventoryUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import de.blazemcworld.blazinggames.computing.ComputerRegistry; +import de.blazemcworld.blazinggames.enchantments.sys.CustomEnchantments; +import de.blazemcworld.blazinggames.enchantments.sys.EnchantmentHelper; +import de.blazemcworld.blazinggames.items.CustomItem; +import org.bukkit.GameMode; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.entity.Player; +import org.bukkit.inventory.Inventory; +import org.bukkit.inventory.ItemStack; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +public class InventoryUtils { + public static boolean canFitItem(Inventory inventory, ItemStack stack) { + stack = stack.clone(); + for (ItemStack curr : inventory) { + if(curr == null || curr.isEmpty()) { + return true; + } + if(curr.isSimilar(stack)) { + int total = curr.getAmount() + stack.getAmount(); + if(total <= curr.getMaxStackSize()) { + return true; + } + stack.setAmount(total - curr.getMaxStackSize()); + } + } + return false; + } + + public static void giveItem(Inventory inventory, ItemStack stack) { + for (ItemStack curr : inventory) { + if(curr.isSimilar(stack)) { + int total = curr.getAmount() + stack.getAmount(); + if(total > curr.getMaxStackSize()) { + curr.setAmount(curr.getMaxStackSize()); + stack.setAmount(total - curr.getMaxStackSize()); + } + else { + curr.setAmount(total); + return; + } + } + } + int slot = inventory.firstEmpty(); + if(slot >= 0) { + inventory.setItem(slot, stack); + } + } + + public static void collectableDrop(Player player, Location location, ItemStack... drops) { + collectableDrop(player, location, Arrays.asList(drops)); + } + + public static void collectableDrop(Player player, Location location, Collection drops) { + if (EnchantmentHelper.hasActiveCustomEnchantment(player.getInventory().getItemInMainHand(), CustomEnchantments.COLLECTABLE)) { + for (ItemStack drop : drops) { + for (Map.Entry overflow : player.getInventory().addItem(drop).entrySet()) { + drop(player, location, overflow.getValue()); + } + } + } else { + for (ItemStack drop : drops) { + drop(player, location, drop); + } + } + } + + private static void drop(Player player, Location location, ItemStack drop) { + if(player.getGameMode() != GameMode.SURVIVAL && player.getGameMode() != GameMode.ADVENTURE) { + // always drop computers (pwease uwu) + if (ComputerRegistry.getComputerByLocationRounded(location) == null) { + if(CustomItem.isCustomItem(drop)) { + return; + } + if(ItemUtils.getUncoloredType(drop) != Material.SHULKER_BOX) { + return; + } + } + } + location.getWorld().dropItemNaturally(location, drop); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/ItemStackTypeAdapter.java b/src/main/java/de/blazemcworld/blazinggames/utils/ItemStackTypeAdapter.java new file mode 100644 index 0000000..5868d47 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/ItemStackTypeAdapter.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Base64; +import java.util.function.Function; + +import org.bukkit.Material; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +public class ItemStackTypeAdapter extends TypeAdapter { + private static Function decompress = s -> GZipToolkit.decompress(Base64.getDecoder().decode(s)); + private static Function compress = s -> Base64.getEncoder().encodeToString(GZipToolkit.compress(s)); + + @Override + public void write(JsonWriter out, ItemStack value) throws IOException { + if (value == null) { out.nullValue(); return; } + out.beginObject(); + out.name("material").value(value.getType().name()); + out.name("amount").value(value.getAmount()); + if (value.hasItemMeta()) { + FileConfiguration metadata = new YamlConfiguration(); + metadata.createSection("data", value.getItemMeta().serialize()); + metadata.getConfigurationSection("data").set("==", ConfigurationSerialization.getAlias(value.getItemMeta().getClass())); + String metadataString = compress.apply(metadata.saveToString()); + out.name("metadata").value(metadataString); + } + out.endObject(); + } + + @Override + public ItemStack read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { in.nextNull(); return null; } + + Material material = null; + Integer amount = null; + String metadataString = null; + + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + if (name.equals("material")) { + material = Material.valueOf(in.nextString()); + } else if (name.equals("amount")) { + amount = in.nextInt(); + } else if (name.equals("metadata")) { + metadataString = in.nextString(); + } + } + in.endObject(); + + if (material == null || amount == null) { + throw new IOException("Could not deserialize: material or amount is null"); + } + + ItemStack item = new ItemStack(material, amount); + + if (metadataString != null) { + FileConfiguration metadata = YamlConfiguration.loadConfiguration(new StringReader(decompress.apply(metadataString))); + item.setItemMeta((ItemMeta) metadata.get("data")); + } + + return item; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/ItemUtils.java b/src/main/java/de/blazemcworld/blazinggames/utils/ItemUtils.java new file mode 100644 index 0000000..830c52c --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/ItemUtils.java @@ -0,0 +1,123 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import org.bukkit.Material; +import org.bukkit.block.Block; +import org.bukkit.inventory.ItemStack; + +public class ItemUtils { + public static Material getUncoloredType(ItemStack stack) { + return getUncoloredType(stack.getType()); + } + + public static Material getUncoloredType(Block block) { + return getUncoloredType(block.getType()); + } + + public static Material getUncoloredType(Material mat) { + return switch(mat) { + case SHULKER_BOX, WHITE_SHULKER_BOX, LIGHT_GRAY_SHULKER_BOX, GRAY_SHULKER_BOX, BLACK_SHULKER_BOX, + BROWN_SHULKER_BOX, RED_SHULKER_BOX, ORANGE_SHULKER_BOX, YELLOW_SHULKER_BOX, LIME_SHULKER_BOX, + GREEN_SHULKER_BOX, CYAN_SHULKER_BOX, LIGHT_BLUE_SHULKER_BOX, BLUE_SHULKER_BOX, PURPLE_SHULKER_BOX, + MAGENTA_SHULKER_BOX, PINK_SHULKER_BOX + -> Material.SHULKER_BOX; + case WHITE_CARPET, LIGHT_GRAY_CARPET, GRAY_CARPET, BLACK_CARPET, BROWN_CARPET, RED_CARPET, ORANGE_CARPET, YELLOW_CARPET, + LIME_CARPET, GREEN_CARPET, CYAN_CARPET, LIGHT_BLUE_CARPET, BLUE_CARPET, PURPLE_CARPET, MAGENTA_CARPET, PINK_CARPET + -> Material.WHITE_CARPET; + case WHITE_BED, LIGHT_GRAY_BED, GRAY_BED, BLACK_BED, BROWN_BED, RED_BED, ORANGE_BED, YELLOW_BED, + LIME_BED, GREEN_BED, CYAN_BED, LIGHT_BLUE_BED, BLUE_BED, PURPLE_BED, MAGENTA_BED, PINK_BED + -> Material.WHITE_BED; + case WHITE_BANNER, LIGHT_GRAY_BANNER, GRAY_BANNER, BLACK_BANNER, BROWN_BANNER, RED_BANNER, ORANGE_BANNER, YELLOW_BANNER, + LIME_BANNER, GREEN_BANNER, CYAN_BANNER, LIGHT_BLUE_BANNER, BLUE_BANNER, PURPLE_BANNER, MAGENTA_BANNER, PINK_BANNER + -> Material.WHITE_BANNER; + case TERRACOTTA, WHITE_TERRACOTTA, LIGHT_GRAY_TERRACOTTA, GRAY_TERRACOTTA, BLACK_TERRACOTTA, BROWN_TERRACOTTA, RED_TERRACOTTA, ORANGE_TERRACOTTA, YELLOW_TERRACOTTA, + LIME_TERRACOTTA, GREEN_TERRACOTTA, CYAN_TERRACOTTA, LIGHT_BLUE_TERRACOTTA, BLUE_TERRACOTTA, PURPLE_TERRACOTTA, MAGENTA_TERRACOTTA, PINK_TERRACOTTA + -> Material.TERRACOTTA; + case WHITE_CONCRETE, LIGHT_GRAY_CONCRETE, GRAY_CONCRETE, BLACK_CONCRETE, BROWN_CONCRETE, RED_CONCRETE, ORANGE_CONCRETE, YELLOW_CONCRETE, + LIME_CONCRETE, GREEN_CONCRETE, CYAN_CONCRETE, LIGHT_BLUE_CONCRETE, BLUE_CONCRETE, PURPLE_CONCRETE, MAGENTA_CONCRETE, PINK_CONCRETE + -> Material.WHITE_CONCRETE; + case WHITE_CONCRETE_POWDER, LIGHT_GRAY_CONCRETE_POWDER, GRAY_CONCRETE_POWDER, BLACK_CONCRETE_POWDER, BROWN_CONCRETE_POWDER, RED_CONCRETE_POWDER, ORANGE_CONCRETE_POWDER, YELLOW_CONCRETE_POWDER, + LIME_CONCRETE_POWDER, GREEN_CONCRETE_POWDER, CYAN_CONCRETE_POWDER, LIGHT_BLUE_CONCRETE_POWDER, BLUE_CONCRETE_POWDER, PURPLE_CONCRETE_POWDER, MAGENTA_CONCRETE_POWDER, PINK_CONCRETE_POWDER + -> Material.WHITE_CONCRETE_POWDER; + case WHITE_GLAZED_TERRACOTTA, LIGHT_GRAY_GLAZED_TERRACOTTA, GRAY_GLAZED_TERRACOTTA, BLACK_GLAZED_TERRACOTTA, BROWN_GLAZED_TERRACOTTA, RED_GLAZED_TERRACOTTA, ORANGE_GLAZED_TERRACOTTA, YELLOW_GLAZED_TERRACOTTA, + LIME_GLAZED_TERRACOTTA, GREEN_GLAZED_TERRACOTTA, CYAN_GLAZED_TERRACOTTA, LIGHT_BLUE_GLAZED_TERRACOTTA, BLUE_GLAZED_TERRACOTTA, PURPLE_GLAZED_TERRACOTTA, MAGENTA_GLAZED_TERRACOTTA, PINK_GLAZED_TERRACOTTA + -> Material.WHITE_GLAZED_TERRACOTTA; + case GLASS, TINTED_GLASS, WHITE_STAINED_GLASS, LIGHT_GRAY_STAINED_GLASS, GRAY_STAINED_GLASS, BLACK_STAINED_GLASS, BROWN_STAINED_GLASS, RED_STAINED_GLASS, ORANGE_STAINED_GLASS, YELLOW_STAINED_GLASS, + LIME_STAINED_GLASS, GREEN_STAINED_GLASS, CYAN_STAINED_GLASS, LIGHT_BLUE_STAINED_GLASS, BLUE_STAINED_GLASS, PURPLE_STAINED_GLASS, MAGENTA_STAINED_GLASS, PINK_STAINED_GLASS + -> Material.GLASS; + case GLASS_PANE, WHITE_STAINED_GLASS_PANE, LIGHT_GRAY_STAINED_GLASS_PANE, GRAY_STAINED_GLASS_PANE, BLACK_STAINED_GLASS_PANE, BROWN_STAINED_GLASS_PANE, RED_STAINED_GLASS_PANE, ORANGE_STAINED_GLASS_PANE, YELLOW_STAINED_GLASS_PANE, + LIME_STAINED_GLASS_PANE, GREEN_STAINED_GLASS_PANE, CYAN_STAINED_GLASS_PANE, LIGHT_BLUE_STAINED_GLASS_PANE, BLUE_STAINED_GLASS_PANE, PURPLE_STAINED_GLASS_PANE, MAGENTA_STAINED_GLASS_PANE, PINK_STAINED_GLASS_PANE + -> Material.GLASS_PANE; + case CANDLE, WHITE_CANDLE, LIGHT_GRAY_CANDLE, GRAY_CANDLE, BLACK_CANDLE, BROWN_CANDLE, RED_CANDLE, ORANGE_CANDLE, YELLOW_CANDLE, + LIME_CANDLE, GREEN_CANDLE, CYAN_CANDLE, LIGHT_BLUE_CANDLE, BLUE_CANDLE, PURPLE_CANDLE, MAGENTA_CANDLE, PINK_CANDLE + -> Material.CANDLE; + case WHITE_WOOL, LIGHT_GRAY_WOOL, GRAY_WOOL, BLACK_WOOL, BROWN_WOOL, RED_WOOL, ORANGE_WOOL, YELLOW_WOOL, + LIME_WOOL, GREEN_WOOL, CYAN_WOOL, LIGHT_BLUE_WOOL, BLUE_WOOL, PURPLE_WOOL, MAGENTA_WOOL, PINK_WOOL + -> Material.WHITE_WOOL; + case OAK_CHEST_BOAT, SPRUCE_CHEST_BOAT, BIRCH_CHEST_BOAT, JUNGLE_CHEST_BOAT, ACACIA_CHEST_BOAT, DARK_OAK_CHEST_BOAT, MANGROVE_CHEST_BOAT, CHERRY_CHEST_BOAT, BAMBOO_CHEST_RAFT + -> Material.OAK_CHEST_BOAT; + case OAK_BOAT, SPRUCE_BOAT, BIRCH_BOAT, JUNGLE_BOAT, ACACIA_BOAT, DARK_OAK_BOAT, MANGROVE_BOAT, CHERRY_BOAT, BAMBOO_RAFT + -> Material.OAK_BOAT; + case OAK_WOOD, SPRUCE_WOOD, BIRCH_WOOD, JUNGLE_WOOD, ACACIA_WOOD, DARK_OAK_WOOD, MANGROVE_WOOD, CHERRY_WOOD, + WARPED_HYPHAE, CRIMSON_HYPHAE + -> Material.OAK_WOOD; + case STRIPPED_OAK_WOOD, STRIPPED_SPRUCE_WOOD, STRIPPED_BIRCH_WOOD, STRIPPED_JUNGLE_WOOD, STRIPPED_ACACIA_WOOD, STRIPPED_DARK_OAK_WOOD, STRIPPED_MANGROVE_WOOD, STRIPPED_CHERRY_WOOD, + STRIPPED_WARPED_HYPHAE, STRIPPED_CRIMSON_HYPHAE + -> Material.STRIPPED_OAK_WOOD; + case OAK_LOG, SPRUCE_LOG, BIRCH_LOG, JUNGLE_LOG, ACACIA_LOG, DARK_OAK_LOG, MANGROVE_LOG, CHERRY_LOG, + WARPED_STEM, CRIMSON_STEM, BAMBOO_BLOCK + -> Material.OAK_LOG; + case STRIPPED_OAK_LOG, STRIPPED_SPRUCE_LOG, STRIPPED_BIRCH_LOG, STRIPPED_JUNGLE_LOG, STRIPPED_ACACIA_LOG, STRIPPED_DARK_OAK_LOG, STRIPPED_MANGROVE_LOG, STRIPPED_CHERRY_LOG, + STRIPPED_WARPED_STEM, STRIPPED_CRIMSON_STEM, STRIPPED_BAMBOO_BLOCK + -> Material.STRIPPED_OAK_LOG; + case OAK_FENCE_GATE, SPRUCE_FENCE_GATE, BIRCH_FENCE_GATE, JUNGLE_FENCE_GATE, ACACIA_FENCE_GATE, DARK_OAK_FENCE_GATE, MANGROVE_FENCE_GATE, CHERRY_FENCE_GATE, + WARPED_FENCE_GATE, CRIMSON_FENCE_GATE, BAMBOO_FENCE_GATE + -> Material.OAK_FENCE_GATE; + case OAK_FENCE, SPRUCE_FENCE, BIRCH_FENCE, JUNGLE_FENCE, ACACIA_FENCE, DARK_OAK_FENCE, MANGROVE_FENCE, CHERRY_FENCE, + WARPED_FENCE, CRIMSON_FENCE, BAMBOO_FENCE + -> Material.OAK_FENCE; + case OAK_SIGN, SPRUCE_SIGN, BIRCH_SIGN, JUNGLE_SIGN, ACACIA_SIGN, DARK_OAK_SIGN, MANGROVE_SIGN, CHERRY_SIGN, + WARPED_SIGN, CRIMSON_SIGN, BAMBOO_SIGN + -> Material.OAK_SIGN; + case OAK_HANGING_SIGN, SPRUCE_HANGING_SIGN, BIRCH_HANGING_SIGN, JUNGLE_HANGING_SIGN, ACACIA_HANGING_SIGN, DARK_OAK_HANGING_SIGN, MANGROVE_HANGING_SIGN, CHERRY_HANGING_SIGN, + WARPED_HANGING_SIGN, CRIMSON_HANGING_SIGN, BAMBOO_HANGING_SIGN + -> Material.OAK_HANGING_SIGN; + case OAK_BUTTON, SPRUCE_BUTTON, BIRCH_BUTTON, JUNGLE_BUTTON, ACACIA_BUTTON, DARK_OAK_BUTTON, MANGROVE_BUTTON, CHERRY_BUTTON, + WARPED_BUTTON, CRIMSON_BUTTON, BAMBOO_BUTTON + -> Material.OAK_BUTTON; + case OAK_DOOR, SPRUCE_DOOR, BIRCH_DOOR, JUNGLE_DOOR, ACACIA_DOOR, DARK_OAK_DOOR, MANGROVE_DOOR, CHERRY_DOOR, + WARPED_DOOR, CRIMSON_DOOR, BAMBOO_DOOR + -> Material.OAK_DOOR; + case OAK_PRESSURE_PLATE, SPRUCE_PRESSURE_PLATE, BIRCH_PRESSURE_PLATE, JUNGLE_PRESSURE_PLATE, ACACIA_PRESSURE_PLATE, DARK_OAK_PRESSURE_PLATE, MANGROVE_PRESSURE_PLATE, CHERRY_PRESSURE_PLATE, + WARPED_PRESSURE_PLATE, CRIMSON_PRESSURE_PLATE, BAMBOO_PRESSURE_PLATE + -> Material.OAK_PRESSURE_PLATE; + case OAK_SLAB, SPRUCE_SLAB, BIRCH_SLAB, JUNGLE_SLAB, ACACIA_SLAB, DARK_OAK_SLAB, MANGROVE_SLAB, CHERRY_SLAB, + WARPED_SLAB, CRIMSON_SLAB, BAMBOO_SLAB, PETRIFIED_OAK_SLAB, BAMBOO_MOSAIC_SLAB + -> Material.OAK_SLAB; + case OAK_STAIRS, SPRUCE_STAIRS, BIRCH_STAIRS, JUNGLE_STAIRS, ACACIA_STAIRS, DARK_OAK_STAIRS, MANGROVE_STAIRS, CHERRY_STAIRS, + WARPED_STAIRS, CRIMSON_STAIRS, BAMBOO_STAIRS, BAMBOO_MOSAIC_STAIRS + -> Material.OAK_STAIRS; + case OAK_TRAPDOOR, SPRUCE_TRAPDOOR, BIRCH_TRAPDOOR, JUNGLE_TRAPDOOR, ACACIA_TRAPDOOR, DARK_OAK_TRAPDOOR, MANGROVE_TRAPDOOR, CHERRY_TRAPDOOR, + WARPED_TRAPDOOR, CRIMSON_TRAPDOOR, BAMBOO_TRAPDOOR + -> Material.OAK_TRAPDOOR; + case OAK_PLANKS, SPRUCE_PLANKS, BIRCH_PLANKS, JUNGLE_PLANKS, ACACIA_PLANKS, DARK_OAK_PLANKS, MANGROVE_PLANKS, CHERRY_PLANKS, + WARPED_PLANKS, CRIMSON_PLANKS, BAMBOO_PLANKS, BAMBOO_MOSAIC + -> Material.OAK_PLANKS; + default -> mat; + }; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/JWTUtils.java b/src/main/java/de/blazemcworld/blazinggames/utils/JWTUtils.java new file mode 100644 index 0000000..a2206d7 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/JWTUtils.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonParser; + +import de.blazemcworld.blazinggames.BlazingGames; +import de.blazemcworld.blazinggames.computing.api.ComputingAPI; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import java.util.Date; +import java.util.concurrent.TimeUnit; +import javax.crypto.SecretKey; + +public class JWTUtils { + private JWTUtils() { + } + + private static SecretKey key() { + return ComputingAPI.getConfig().jwtSecretKey(); + } + + public static String sign(JsonElement body, String issuer, TimeUnit unit, long duration) { + return sign(body.toString(), issuer, unit.toMillis(duration)); + } + + public static String sign(JsonElement body, String issuer, long millis) { + return sign(body.toString(), issuer, millis); + } + + public static String sign(String body, String issuer, TimeUnit unit, long duration) { + return sign(body, issuer, unit.toMillis(duration)); + } + + public static String sign(String body, String issuer, long millis) { + return Jwts.builder() + .notBefore(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + millis)) + .subject(body) + .issuer(issuer) + .signWith(key()) + .compact(); + } + + public static String parseToString(String jwt, String issuer) { + try { + Claims claims = (Claims)Jwts.parser().verifyWith(key()).requireIssuer(issuer).build().parseSignedClaims(jwt).getPayload(); + Long exp = (Long)claims.get("exp", Long.class); + Long nbf = (Long)claims.get("nbf", Long.class); + long realTime = System.currentTimeMillis() / 1000L; + if (exp == null || nbf == null) { + return null; + } else { + return realTime >= nbf && realTime <= exp ? claims.getSubject() : null; + } + } catch (IllegalArgumentException | JwtException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + public static JsonElement parseToJson(String jwt, String issuer) { + String string = parseToString(jwt, issuer); + if (string == null) { + return null; + } else { + try { + return JsonParser.parseString(string); + } catch (JsonParseException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + } + + public static JsonObject parseToJsonObject(String jwt, String issuer) { + try { + return GetGson.getAsObject(parseToJson(jwt, issuer), new IllegalArgumentException("Invalid JWT")); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + return null; + } + } + + public static JsonArray parseToJsonArray(String jwt, String issuer) { + try { + return GetGson.getAsArray(parseToJson(jwt, issuer), new IllegalArgumentException("Invalid JWT")); + } catch (IllegalArgumentException e) { + BlazingGames.get().debugLog(e); + return null; + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/NameGenerator.java b/src/main/java/de/blazemcworld/blazinggames/utils/NameGenerator.java new file mode 100644 index 0000000..8d47b8b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/NameGenerator.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +public class NameGenerator { + private static final String[] adverbs = new String[]{ + "a", "b", "c", "d", "e", "f", + "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", + "s", "t", "u", "v", "w", "x", + "y", "z", ".", ",", "!", "?" + }; + private static final String[] adjectives = new String[]{ + "a", "b", "c", "d", "e", "f", + "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", + "s", "t", "u", "v", "w", "x", + "y", "z", ".", ",", "!", "?" + }; + private static final String[] nouns = new String[]{ + "a", "b", "c", "d", "e", "f", + "g", "h", "i", "j", "k", "l", + "m", "n", "o", "p", "q", "r", + "s", "t", "u", "v", "w", "x", + "y", "z", ".", ",", "!", "?" + }; + + private NameGenerator() {} + + public static String generateName() { + return adverbs[(int) (Math.random() * adverbs.length)] + + " " + + adjectives[(int) (Math.random() * adjectives.length)] + + " " + + nouns[(int) (Math.random() * nouns.length)]; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/NamespacedKeyDataType.java b/src/main/java/de/blazemcworld/blazinggames/utils/NamespacedKeyDataType.java new file mode 100644 index 0000000..0d3c442 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/NamespacedKeyDataType.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import org.bukkit.NamespacedKey; +import org.bukkit.persistence.PersistentDataAdapterContext; +import org.bukkit.persistence.PersistentDataType; +import org.jetbrains.annotations.NotNull; + +public class NamespacedKeyDataType implements PersistentDataType { + public static final NamespacedKeyDataType instance = new NamespacedKeyDataType(); + + private NamespacedKeyDataType() { + + } + + @Override + public @NotNull Class getPrimitiveType() { + return String.class; + } + + @Override + public @NotNull Class getComplexType() { + return NamespacedKey.class; + } + + @Override + public @NotNull String toPrimitive(@NotNull NamespacedKey complex, @NotNull PersistentDataAdapterContext context) { + return complex.toString(); + } + + @Override + public @NotNull NamespacedKey fromPrimitive(@NotNull String primitive, @NotNull PersistentDataAdapterContext context) { + NamespacedKey key = NamespacedKey.fromString(primitive); + + if(key == null) { + throw new IllegalArgumentException("Somehow, somewhere, a key which was not valid has appeared. " + primitive); + } + + return key; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/NumberUtils.java b/src/main/java/de/blazemcworld/blazinggames/utils/NumberUtils.java new file mode 100644 index 0000000..9314709 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/NumberUtils.java @@ -0,0 +1,38 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +public class NumberUtils { + public static String getRomanNumber(int number) { + if(number == 0) return "0"; + if(number < 0) return "-" + getRomanNumber(-number); + + + return "I".repeat(number) + .replace("IIIII", "V") + .replace("IIII", "IV") + .replace("VV", "X") + .replace("VIV", "IX") + .replace("XXXXX", "L") + .replace("XXXX", "XL") + .replace("LL", "C") + .replace("LXL", "XC") + .replace("CCCCC", "D") + .replace("CCCC", "CD") + .replace("DD", "M") + .replace("DCD", "CM"); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/Pair.java b/src/main/java/de/blazemcworld/blazinggames/utils/Pair.java new file mode 100644 index 0000000..00cb4a0 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/Pair.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +public class Pair { + public L left; + public R right; + + public Pair(L left, R right) { + this.left = left; + this.right = right; + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/PlayerConfig.java b/src/main/java/de/blazemcworld/blazinggames/utils/PlayerConfig.java new file mode 100644 index 0000000..48021e0 --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/PlayerConfig.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.util.Properties; +import java.util.UUID; + +import de.blazemcworld.blazinggames.BlazingGames; +import net.kyori.adventure.text.format.TextColor; + +public class PlayerConfig { + private static final File prefsDir = new File("prefs"); + static { + if (!prefsDir.exists()) { + prefsDir.mkdir(); + } + + if (!prefsDir.isDirectory()) { + throw new RuntimeException("prefsDir is not a directory"); + } + } + + public static PlayerConfig forPlayer(UUID uuid) { + File file = new File(prefsDir, uuid.toString() + ".properties"); + if (!file.exists()) { + try { + file.createNewFile(); + } catch (IOException e) { + BlazingGames.get().log(e); + return null; + } + } + + return new PlayerConfig(file); + } + + private Properties props; + private File file; + private PlayerConfig(File file) { + this.props = new Properties(); + this.file = file; + + try { + props.load(new FileReader(file)); + } catch (IOException e) { + BlazingGames.get().log(e); + } + } + + private void write() { + try { + props.store(new FileOutputStream(file), null); + } catch (IOException e) { + BlazingGames.get().log(e); + } + } + + + + public String getDisplayName() { + String value = props.getProperty("displayname", null); + if (value == null || value.isBlank()) return null; + return value; + } + + public void setDisplayName(String name) { + if (name == null || name.isBlank()) props.remove("displayname"); + else props.setProperty("displayname", name); + write(); + } + + + + public String getPronouns() { + String value = props.getProperty("pronouns", null); + if (value == null || value.isBlank()) return null; + return value; + } + + public void setPronouns(String pronouns) { + if (pronouns == null || pronouns.isBlank()) props.remove("pronouns"); + else props.setProperty("pronouns", pronouns); + write(); + } + + + + public TextColor getNameColor() { + return TextColor.color(Integer.valueOf(props.getProperty("namecolor", "16777215"))); + } + + public void setNameColor(TextColor color) { + if (color == null) props.remove("namecolor"); + else props.setProperty("namecolor", String.valueOf(color.value())); + write(); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/TextLocation.java b/src/main/java/de/blazemcworld/blazinggames/utils/TextLocation.java new file mode 100644 index 0000000..c0a50fe --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/TextLocation.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import java.io.IOException; + +import org.bukkit.Bukkit; +import org.bukkit.Location; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Location represented as a string + */ +public class TextLocation { + private TextLocation() { + } + + public static String serialize(Location location) { + return location == null ? null + : location.getWorld().getName() + + " " + + location.getX() + + " " + + location.getY() + + " " + + location.getZ() + + " " + + location.getYaw() + + " " + + location.getPitch(); + } + + public static String serializeRounded(Location location) { + return location == null ? null + : location.getWorld().getName() + + " " + + location.blockX() + + " " + + location.blockY() + + " " + + location.blockZ() + + " 0.0 0.0"; + } + + public static Location deserialize(String serialized) { + if (serialized != null && !serialized.isEmpty()) { + String[] split = serialized.split(" "); + String worldName = split[0]; + double x = Double.parseDouble(split[1]); + double y = Double.parseDouble(split[2]); + double z = Double.parseDouble(split[3]); + float yaw = Float.parseFloat(split[4]); + float pitch = Float.parseFloat(split[5]); + return new Location(Bukkit.getWorld(worldName), x, y, z, yaw, pitch); + } else { + return null; + } + } + + public static class LocationTypeAdapter extends TypeAdapter { + @Override + public void write(JsonWriter out, Location value) throws IOException { + out.value(TextLocation.serialize(value)); + } + + @Override + public Location read(JsonReader in) throws IOException { + if (in.peek() != JsonToken.STRING) return null; + String value = in.nextString(); + return TextLocation.deserialize(value); + } + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/TextUtils.java b/src/main/java/de/blazemcworld/blazinggames/utils/TextUtils.java new file mode 100644 index 0000000..eac112e --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/TextUtils.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; + +public class TextUtils { + public static String componentToString(Component component) { + return LegacyComponentSerializer.legacySection().serialize(component); + } + + public static Component stringToComponent(String string) { + return Component.text(string); + } + + public static Component colorCodeParser(Component message) { + String text = componentToString(message) + .replaceAll("&([a-fk-or0-9])", "§$1"); + return Component.text(text); + } + + public static String stripColorCodes(String message) { + return message.replaceAll("&[0-9a-fk-or]", ""); + } +} diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/TomeAltarStorage.java b/src/main/java/de/blazemcworld/blazinggames/utils/TomeAltarStorage.java new file mode 100644 index 0000000..dea14bd --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/TomeAltarStorage.java @@ -0,0 +1,71 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +import de.blazemcworld.blazinggames.data.DataStorage; +import de.blazemcworld.blazinggames.data.compression.GZipCompressionProvider; +import de.blazemcworld.blazinggames.data.providers.ArbitraryNameProvider; +import de.blazemcworld.blazinggames.data.storage.GsonStorageProvider; + +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.inventory.ItemStack; +import java.util.List; + +public class TomeAltarStorage { + private static DataStorage dataStorage = DataStorage.forClass( + TomeAltarStorage.class, null, + new GsonStorageProvider<>(ItemStack.class), new ArbitraryNameProvider(), new GZipCompressionProvider() + ); + + public static void addTomeAltar(Location location) { + dataStorage.storeData(TextLocation.serializeRounded(location), null); + } + + public static boolean isTomeAltar(Location location) { + return dataStorage.hasData(TextLocation.serializeRounded(location)); + } + + public static void setItem(Location location, ItemStack item) { + String key = TextLocation.serializeRounded(location); + if (dataStorage.hasData(key)) { + dataStorage.storeData(key, item); + } + } + + public static ItemStack getItem(Location location) { + return dataStorage.getData(TextLocation.serializeRounded(location), null); + } + + public static void removeTomeAltar(Location location) { + dataStorage.deleteData(TextLocation.serializeRounded(location)); + } + + public static List getAll(World world) { + return dataStorage.queryIdentifiers(i -> { + return world.equals(TextLocation.deserialize(i).getWorld()); + }).stream().map(TextLocation::deserialize).toList(); + } + + public static List getNear(Location loc, int radius) { + int radiusSquared = radius * radius; + return dataStorage.queryIdentifiers(i -> { + Location location = TextLocation.deserialize(i); + if (!location.getWorld().equals(loc.getWorld())) return false; + return loc.getWorld().equals(location.getWorld()) && loc.distanceSquared(location) < radiusSquared; + }).stream().map(TextLocation::deserialize).toList(); + } +} \ No newline at end of file diff --git a/src/main/java/de/blazemcworld/blazinggames/utils/Triple.java b/src/main/java/de/blazemcworld/blazinggames/utils/Triple.java new file mode 100644 index 0000000..0732c5b --- /dev/null +++ b/src/main/java/de/blazemcworld/blazinggames/utils/Triple.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 The Blazing Games Maintainers + * + * 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 + * + * http://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. + */ +package de.blazemcworld.blazinggames.utils; + +public class Triple { + public L left; + public M middle; + public R right; + + public Triple(L left, M middle, R right) { + this.left = left; + this.middle = middle; + this.right = right; + } + + public Pair left2() { + return new Pair<>(left, middle); + } + + public Pair right2() { + return new Pair<>(middle, right); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..2d29122 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,76 @@ +jda: + enabled: false + token: "___.___.___" + link-channel: 000000000000000000 + console-channel: 000000000000000000 + webhook: "https://discord.com/api/webhooks/...." + +logging: + log-error: true + log-info: true + log-debug: false + notify-ops-on-error: true + +computing: + local: + disable-computers: false + privileges: + chunkloading: false + net: false + microsoft: + spoof-ms-server: true + client-id: "chat is this real" + client-secret: "this is so real" + jwt: + secret-key: randomize-on-server-start + secret-key-is-password: false + services: + blazing-api: + enabled: false + find-at: "http://localhost:8080" + bind: + port: 8080 + https: + enabled: false + password: "enter super secret and secure password here" + file: localhost.p12 + proxy: + in-use: false + ip-address-header: CF-Connecting-IP + allow-all: false + allowed-ipv4: [] + allowed-ipv6: [] + blazing-wss: + enabled: false + find-at: "http://localhost:9090" + bind: + port: 9090 + https: + enabled: false + password: "enter super secret and secure password here" + file: localhost.p12 + proxy: + in-use: false + ip-address-header: CF-Connecting-IP + allow-all: false + allowed-ipv4: [] + allowed-ipv6: [] + +docs: + official-instance: + name: Blazing Console + url: https://console.bedcrab.dev + user-links: + - name: Discord + url: "#" + developer-links: + - name: Support + url: "#" + - name: Bug Tracker + url: "#" + notice: + show: true + title: Change (or disable) this notice bar in the config.yml + description: Show important announcements or links here + button-title: Customizable link button + button-url: "#" \ No newline at end of file diff --git a/src/main/resources/html/codeinput.html b/src/main/resources/html/codeinput.html new file mode 100644 index 0000000..d4335a0 --- /dev/null +++ b/src/main/resources/html/codeinput.html @@ -0,0 +1,85 @@ + + + + + + + + Link with BlazingGames + + + +
+ <#if error> +
+

+ The code you entered was invalid or expired. +

+
+
+ +

Link an application to BlazingGames

+
+ + <#if offline> + + + + +
+

+ Enter the code the application gave you above. +

+
+ How it works +

+ After entering a code above, if it is valid, you'll be redirected + to the Microsoft sign in page. Select your account and make sure + it has a Minecraft account linked to it. You'll be asked if you want + to authorize the application, and it will be linked. Linking lasts + for 6 hours and gives the application access to your in game computers + and turtles, where they can update code, start and stop, and more. +

+ You can also add collaborators and transfer ownership, but both of these + actions require confirmation from you in game, which can be disabled when + linking the application. +

+
+
+ Unlink applications +

+ If you want to remove an application, you can unlink every currently linked + application at once. If you're interested, click the button below: +

+
+ +
+
+
+ Get help +

+ Visit the Discord server if you need help. +

+
+
+ + \ No newline at end of file diff --git a/src/main/resources/html/consent.html b/src/main/resources/html/consent.html new file mode 100644 index 0000000..3375a6b --- /dev/null +++ b/src/main/resources/html/consent.html @@ -0,0 +1,126 @@ + + + + + + + + Link to ${appname} + + + +
+ +
+
+

Link to ${appname}

+

${apppurpose} - by ${appcontact}

+ +
+ + This will allow the developers of this application to: + + + + + + + + + + + + + <#list permissions as permission, risk> + + + + + + +
PermissionRisk level
View your Minecraft Username, UUID and skinNone
${permission}${risk}
+ + +

+ This access will expire in 6 hours automatically. If you want to revoke access before that, you can visit /auth/link, which has instructions for revoking access. +

+ +
+ +
+ Debugging information +
    +
  • Link code: ${code}
  • +
  • Verification token: ${token}
  • +
+

Do not share the details above!

+
+ + Thank you to Crafatar for the player head renders. +
+ + + \ No newline at end of file diff --git a/src/main/resources/html/docs.html b/src/main/resources/html/docs.html new file mode 100644 index 0000000..8e065b3 --- /dev/null +++ b/src/main/resources/html/docs.html @@ -0,0 +1,526 @@ + + + + + + + + + Blazing API + + + +
+ + +
+
+

Welcome to the Blazing API website!

+

You're probably looking for one of these:

+
+ ${instance.label} + Link an application + Unlink applications + <#list playerurls as playerurl> + ${playerurl.label} + +
+
+
+ + +
+ + +
+ +
+
+ + <#list endpointcategories as listcategory, listendpoints> +

${listcategory}

+ + + + +
+
+ <#list endpointcategories as bodycategory, bodyendpoints> +

${bodycategory}

+ <#list bodyendpoints as bodyendpoint> +
+

${bodyendpoint.method} ${bodyendpoint.path} (${bodyendpoint.title})

+

${bodyendpoint.description}

+
${bodyendpoint.paramstitle}
+
    + <#list bodyendpoint.incoming as incomingid, incomingdescription> +
  • ${incomingid}: ${incomingdescription}
  • + <#else> +
  • None
  • + +
+
Response Body
+
    + <#list bodyendpoint.outgoing as outgoingid, outgoingdescription> +
  • ${outgoingid}: ${outgoingdescription}
  • + <#else> +
  • None
  • + +
+
Response Codes
+
    + <#list bodyendpoint.responsecodes as responsecodeid, responsecodedescription> +
  • ${responsecodeid}: ${responsecodedescription}
  • + <#else> +
  • None?
  • + +
+
Required Permissions
+
    + <#list bodyendpoint.permissions as permissionid, permissiondescription> +
  • ${permissionid}: ${permissiondescription}
  • + <#else> +
  • None
  • + +
+
+
+ + +

You've reached the bottom. Back to top?

+ + +
+
+
+ + + \ No newline at end of file diff --git a/src/main/resources/html/error.html b/src/main/resources/html/error.html new file mode 100644 index 0000000..d2eca9d --- /dev/null +++ b/src/main/resources/html/error.html @@ -0,0 +1,42 @@ + + + + + + + + Error + + + +
+

An error occurred

+

${error}

+
+ More details +

${desc}

+
+

Contact the server developers if the error persists.

+ +
+ + \ No newline at end of file diff --git a/src/main/resources/html/unlink.html b/src/main/resources/html/unlink.html new file mode 100644 index 0000000..44e0024 --- /dev/null +++ b/src/main/resources/html/unlink.html @@ -0,0 +1,78 @@ + + + + + + + + Unlink applications + + + +
+ +
+
+

Unlink All Applications

+

+ This will revoke all applications linked to your account. + This can be undone by linking the application again. +

+ +
+ + + diff --git a/src/main/resources/html/verdict.html b/src/main/resources/html/verdict.html new file mode 100644 index 0000000..5ddf633 --- /dev/null +++ b/src/main/resources/html/verdict.html @@ -0,0 +1,51 @@ + + + + + + + + You can now close this window or tab + + + +
+ +
+
+

${title}

+

${body}

+
+

It is now safe to close the window or tab.

+ +
+ + diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..84beb62 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,59 @@ +name: blazinggames +version: '${version}' +main: de.blazemcworld.blazinggames.BlazingGames +api-version: '1.21' +prefix: BlazingGames +authors: [BlazeMCworld, sbot50, 'Ivy Collective (ivyc.)', XTerPL] +description: funny block game javascript computers +website: https://blazingsite.surge.sh/ + +permissions: + blazinggames.customenchant: + description: "Allows using the /customenchant command" + default: op + blazinggames.customgive: + description: "Allows using the /customgive command" + default: op + blazinggames.setaltar: + description: "Allows using the /setalatar command" + default: op + +libraries: ## see build.gradle + - io.azam.ulidj:ulidj:1.0.4 + - net.dv8tion:JDA:5.0.0-beta.23 + - club.minnced:discord-webhooks:0.8.4 + - com.zaxxer:HikariCP:5.1.0 + - org.xerial:sqlite-jdbc:3.45.3.0 + - com.mysql:mysql-connector-j:8.4.0 + - org.postgresql:postgresql:42.7.3 + - com.caoccao.javet:javet:3.1.2 + - com.github.ben-manes.caffeine:caffeine:3.1.8 + - io.jsonwebtoken:jjwt-api:0.12.6 + - io.jsonwebtoken:jjwt-impl:0.12.6 + - io.jsonwebtoken:jjwt-gson:0.12.6 + - org.freemarker:freemarker:2.3.33 + - org.java-websocket:Java-WebSocket:1.5.7 + +commands: + customenchant: + description: "Enchants the main hand item with a specific custom enchantment" + usage: /customenchant [level] + permission: blazinggames.customenchant + customgive: + description: "Gives you a specific custom item with a specified count" + usage: /customgive [count] + permission: blazinggames.customgive + killme: + description: "Kills you. Painfully." + usage: /killme + aliases: [suicide] + playtime: + description: "See how much time you and your friends have wasted on this stupid server." + usage: /playtime [player] + config: + description: "Change display settings for your player. Tab complete for available options." + usage: /config [...value] + setaltar: + description: "Set altar with specific level at current location" + usage: /setaltar + permission: blazinggames.setmultiblock