mirror of
https://github.com/BlazingGames/blazing-games-plugin.git
synced 2025-02-03 21:26:41 -05:00
init
This commit is contained in:
commit
503dcf5ef9
207 changed files with 19196 additions and 0 deletions
58
.forgejo/workflows/build.yml
Normal file
58
.forgejo/workflows/build.yml
Normal file
|
@ -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
|
124
.gitignore
vendored
Normal file
124
.gitignore
vendored
Normal file
|
@ -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/
|
40
.vscode/settings.json
vendored
Normal file
40
.vscode/settings.json
vendored
Normal file
|
@ -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 <<start>> <<author>>",
|
||||||
|
"",
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
}
|
163
CONFIG.md
Normal file
163
CONFIG.md
Normal file
|
@ -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
|
202
LICENSE
Normal file
202
LICENSE
Normal file
|
@ -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.
|
2
NOTICE
Normal file
2
NOTICE
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
Copyright 2025 The Blazing Games Maintainers <blazing-games@bedcrab.dev>
|
||||||
|
This software is free software, licensed under the Apache License (vesion 2.0). For more information, please read the LICENSE file.
|
65
PROTOCOL.md
Normal file
65
PROTOCOL.md
Normal file
|
@ -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
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -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.
|
||||||
|
|
83
build.gradle
Normal file
83
build.gradle
Normal file
|
@ -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)
|
||||||
|
}
|
1
gradle.properties
Normal file
1
gradle.properties
Normal file
|
@ -0,0 +1 @@
|
||||||
|
version = STAGING
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -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
|
249
gradlew
vendored
Normal file
249
gradlew
vendored
Normal file
|
@ -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" "$@"
|
92
gradlew.bat
vendored
Normal file
92
gradlew.bat
vendored
Normal file
|
@ -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
|
10
settings.gradle
Normal file
10
settings.gradle
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven {
|
||||||
|
url 'https://repo.papermc.io/repository/maven-public/'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rootProject.name = 'blazinggames'
|
295
src/main/java/de/blazemcworld/blazinggames/BlazingGames.java
Normal file
295
src/main/java/de/blazemcworld/blazinggames/BlazingGames.java
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<BuilderLocation> 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<BuilderLocation> getBuilderWandLocations(Player player, BlockFace face, BuilderLocation location, BuilderWandMode mode, Material placementMaterial, int maxBlocks) {
|
||||||
|
List<BuilderLocation> locations = new ArrayList<>();
|
||||||
|
locations.add(location);
|
||||||
|
|
||||||
|
List<BlockFace> 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<NamespacedKey, Recipe> 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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<BuilderWandMode> persistentType = new EnumDataType<>(BuilderWandMode.class);
|
||||||
|
|
||||||
|
private final String modeText;
|
||||||
|
private final List<BlockFace> allowedFaces = new ArrayList<>();
|
||||||
|
private final List<BlockFace> buildDirections = new ArrayList<>();
|
||||||
|
|
||||||
|
BuilderWandMode(String modeText, Consumer<BuilderWandMode> builder) {
|
||||||
|
this.modeText = modeText;
|
||||||
|
builder.accept(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canBuildOnFace(BlockFace face) {
|
||||||
|
return allowedFaces.contains(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<BlockFace> getBuildDirections() {
|
||||||
|
ImmutableList.Builder<BlockFace> 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command,
|
||||||
|
@NotNull String s, @NotNull String[] strings) {
|
||||||
|
List<String> tabs = new ArrayList<>();
|
||||||
|
|
||||||
|
if(strings.length == 1) {
|
||||||
|
CustomEnchantments.list().forEach(enchantment -> tabs.add(enchantment.getKey().getKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command,
|
||||||
|
@NotNull String s, @NotNull String[] strings) {
|
||||||
|
List<String> tabs = new ArrayList<>();
|
||||||
|
|
||||||
|
if(strings.length == 1) {
|
||||||
|
CustomItems.list().forEach(itemType -> tabs.add(itemType.getKey().getKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabs;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Vector, BlockPredicate> 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<String> onTabComplete(@NotNull CommandSender commandSender, @NotNull Command command,
|
||||||
|
@NotNull String s, @NotNull String[] strings) {
|
||||||
|
return List.of("1", "2", "3", "4");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> upgrades;
|
||||||
|
private UUID owner;
|
||||||
|
private ArrayList<UUID> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ComputerMetadata> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<BootedComputer> 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<ComputerMetadata, String> metadataStorage = DataStorage.forClass(
|
||||||
|
ComputerRegistry.class, "metadata",
|
||||||
|
new GsonStorageProvider<ComputerMetadata>(ComputerMetadata.class),
|
||||||
|
new ULIDNameProvider(), new GZipCompressionProvider()
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final DataStorage<byte[], String> stateStorage = DataStorage.forClass(
|
||||||
|
ComputerRegistry.class, "state",
|
||||||
|
new BinaryStorageProvider(), new ULIDNameProvider(), new GZipCompressionProvider()
|
||||||
|
);
|
||||||
|
|
||||||
|
public static final DataStorage<String, String> 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<Boolean, BootedComputer> 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<BootedComputer> callback) {
|
||||||
|
if (getComputerByLocationRounded(location) != null) {
|
||||||
|
throw new IllegalArgumentException("Computer already exists at " + location);
|
||||||
|
} else {
|
||||||
|
final Pair<ComputerMetadata, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String> incomingArgs,
|
||||||
|
HashMap<String, String> outgoingArgs,
|
||||||
|
HashMap<Integer, String> responseCodes,
|
||||||
|
List<Permission> 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<String, String> incomingArgs = new HashMap<>();
|
||||||
|
private final HashMap<String, String> outgoingArgs = new HashMap<>();
|
||||||
|
private final HashMap<Integer, String> responseCodes = new HashMap<>();
|
||||||
|
private final ArrayList<Permission> 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<String, String> 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<String, String> 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<Integer, String> 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<Permission> permissions() {
|
||||||
|
return List.copyOf(this.permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, List<String>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> proxyAllowedIPV4,
|
||||||
|
List<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String> headers;
|
||||||
|
final String body;
|
||||||
|
private static final Configuration config = new Configuration(Configuration.VERSION_2_3_33);
|
||||||
|
|
||||||
|
public EndpointResponse(int status, HashMap<String, String> 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<String, Object> map) {
|
||||||
|
Map<String, Object> 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<String, String> 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<String, String> 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<String, String> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Permission> 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<Permission> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, List<String>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<RequestMethod.Flag> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Pair<TokenManager.AuthState, TokenManager.ApplicationClaim>> preAuthKeys = Caffeine.newBuilder()
|
||||||
|
.maximumSize(1000L)
|
||||||
|
.expireAfterWrite(Duration.ofMinutes(10L))
|
||||||
|
.build();
|
||||||
|
private static final Cache<String, TokenManager.Profile> unlinkRequests = Caffeine.newBuilder()
|
||||||
|
.maximumSize(1000L)
|
||||||
|
.expireAfterWrite(Duration.ofMinutes(10L))
|
||||||
|
.build();
|
||||||
|
private static final Cache<UUID, Integer> 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<TokenManager.AuthState, TokenManager.ApplicationClaim> value = (Pair<TokenManager.AuthState, TokenManager.ApplicationClaim>)preAuthKeys.getIfPresent(
|
||||||
|
code
|
||||||
|
);
|
||||||
|
return value == null ? null : value.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TokenManager.ApplicationClaim getApplicationClaimFromCode(String code) {
|
||||||
|
Pair<TokenManager.AuthState, TokenManager.ApplicationClaim> value = (Pair<TokenManager.AuthState, TokenManager.ApplicationClaim>)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<TokenManager.AuthState, TokenManager.ApplicationClaim> previousValue = (Pair<TokenManager.AuthState, TokenManager.ApplicationClaim>)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<Permission> 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 {}
|
||||||
|
}
|
|
@ -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<String, Object> root = new HashMap<>();
|
||||||
|
FileConfiguration config = BlazingGames.get().getConfig();
|
||||||
|
|
||||||
|
HashMap<String, Object> 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<String, Object> 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<HashMap<String, Object>> playerurls = new ArrayList<>();
|
||||||
|
config.getMapList("docs.user-links").forEach(map -> {
|
||||||
|
HashMap<String, Object> url = new HashMap<>();
|
||||||
|
url.put("label", map.get("name"));
|
||||||
|
url.put("url", map.get("url"));
|
||||||
|
playerurls.add(url);
|
||||||
|
});
|
||||||
|
root.put("playerurls", playerurls);
|
||||||
|
|
||||||
|
ArrayList<HashMap<String, Object>> devurls = new ArrayList<>();
|
||||||
|
config.getMapList("docs.developer-links").forEach(map -> {
|
||||||
|
HashMap<String, Object> url = new HashMap<>();
|
||||||
|
url.put("label", map.get("name"));
|
||||||
|
url.put("url", map.get("url"));
|
||||||
|
devurls.add(url);
|
||||||
|
});
|
||||||
|
root.put("devurls", devurls);
|
||||||
|
|
||||||
|
HashMap<String, ArrayList<HashMap<String, Object>>> 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<String, Object> 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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, String> permissions = new HashMap<>();
|
||||||
|
|
||||||
|
for (Permission p : appClaim.permissions()) {
|
||||||
|
permissions.put(p.description, p.level.display);
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<String, Object> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> 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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Permission> 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 <code>/auth/link</code>.")
|
||||||
|
.addOutgoingArgument("key", "Key which will allow you to retrieve the real token after the flow is finished. See <code>/auth/redeem</code>.")
|
||||||
|
.addGenericsUnauthenthicated()
|
||||||
|
.build()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <code>/auth/prepare</code> 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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String, Object> 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<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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<IComputerType> type;
|
||||||
|
private final Class<? extends IComputerType> clazz;
|
||||||
|
|
||||||
|
private ComputerTypes(Supplier<IComputerType> type, Class<? extends IComputerType> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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];
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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<String, JsonObject> 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<String, JsonObject> 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) {}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<JsonObject, ServerboundPacket> factory;
|
||||||
|
Type(Function<JsonObject, ServerboundPacket> factory) {
|
||||||
|
this.factory = factory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ItemStack> hotbarItems;
|
||||||
|
public final List<ItemStack> inventoryItems;
|
||||||
|
|
||||||
|
public CrateData(String id, UUID owner, boolean opened, Location location, int exp,
|
||||||
|
ItemStack helmet, ItemStack chestplate, ItemStack leggings, ItemStack boots, ItemStack offhand,
|
||||||
|
List<ItemStack> hotbarItems, List<ItemStack> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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<CrateData, String> 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<String> 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<ItemStack> items = new ArrayList<>();
|
||||||
|
for (ItemStack item : inventory.getStorageContents()) {
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ItemStack> hotbarItems = items.subList(0, 9).stream().filter(i -> i == null ? true : CrateManager.shouldStayOnDeath(i)).toList();
|
||||||
|
List<ItemStack> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
350
src/main/java/de/blazemcworld/blazinggames/data/DataStorage.java
Normal file
350
src/main/java/de/blazemcworld/blazinggames/data/DataStorage.java
Normal file
|
@ -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<T, I> {
|
||||||
|
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 <T> The type of data being stored
|
||||||
|
* @param <I> 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 <T, I> DataStorage<T, I> forClass(Class<?> clazz, String tableId, StorageProvider<T> storage, NameProvider<I> 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 <T> The type of data being stored
|
||||||
|
// * @param <I> 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 <T, I> DataStorage<T, I> forClass(Class<?> clazz, StorageProvider<T> storage, NameProvider<I> name, CompressionProvider compression, Consumer<IndexContext<T, I>> 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<T> storage;
|
||||||
|
public final NameProvider<I> name;
|
||||||
|
public final CompressionProvider compression;
|
||||||
|
public DataStorage(File dir, StorageProvider<T> storage, NameProvider<I> name, CompressionProvider compression) {
|
||||||
|
this.dir = dir;
|
||||||
|
this.storage = storage;
|
||||||
|
this.name = name;
|
||||||
|
this.compression = compression;
|
||||||
|
// this.indexer = null;
|
||||||
|
// this.index = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// public final Consumer<IndexContext<T, I>> indexer;
|
||||||
|
// public final HashMap<String, Object> index;
|
||||||
|
// public DataStorage(File dir, StorageProvider<T> storage, NameProvider<I> name, CompressionProvider compression, Consumer<IndexContext<T, I>> 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<File> callback) {
|
||||||
|
useFile(identifier, file -> {
|
||||||
|
callback.accept(file);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronized file usage (with return value)
|
||||||
|
*/
|
||||||
|
private synchronized <V> V useFile(I identifier, Function<File, V> 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<String> 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<T, I> storeNext(Function<I, T> 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> V queryIndex(String key, Class<V> 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<I> query(Predicate<T> predicate) {
|
||||||
|
try (Stream<Path> 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<T> queryForData(Predicate<T> 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<I> queryIdentifiers(Predicate<I> predicate) {
|
||||||
|
try (Stream<Path> 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<T> queryIdentifiersForData(Predicate<I> 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T> {
|
||||||
|
public abstract T next();
|
||||||
|
public abstract String fromValue(T value);
|
||||||
|
public abstract T fromString(String string);
|
||||||
|
}
|
|
@ -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<T> {
|
||||||
|
public abstract String fileExtension();
|
||||||
|
public abstract T read(byte[] data);
|
||||||
|
public abstract byte[] write(T data);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> {
|
||||||
|
protected final Supplier<String> supplier;
|
||||||
|
|
||||||
|
|
||||||
|
public ArbitraryNameProvider() {
|
||||||
|
this.supplier = () -> null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArbitraryNameProvider(final String value) {
|
||||||
|
this.supplier = () -> value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ArbitraryNameProvider(final Supplier<String> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<String> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<UUID> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<byte[]> {
|
||||||
|
@Override
|
||||||
|
public String fileExtension() {
|
||||||
|
return "bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] read(byte[] data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] write(byte[] data) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<T> extends StorageProvider<T> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<String> {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
) { }
|
|
@ -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 <T> Component formatArrayIntoComponent(
|
||||||
|
String title, T[] items, Function<T, Component> chatText,
|
||||||
|
Function<T, Component> hoverText, Function<T, String> 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Triple<Integer, Integer, Material>> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Material> allowed;
|
||||||
|
|
||||||
|
BlazingEnchantmentTarget(Material... allowed) {
|
||||||
|
this.allowed = Set.of(allowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean includes(@NotNull Material item) {
|
||||||
|
return allowed.contains(item);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<CustomEnchantment, Integer> customEnchantmentLevels = EnchantmentHelper.getCustomEnchantments(itemStack);
|
||||||
|
|
||||||
|
for(Map.Entry<CustomEnchantment, Integer> entry : customEnchantmentLevels.entrySet()) {
|
||||||
|
if(conflictsWith(entry.getKey()) || entry.getKey().conflictsWith(this)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ItemMeta meta = itemStack.getItemMeta();
|
||||||
|
|
||||||
|
Map<Enchantment, Integer> enchantmentLevels = Map.of();
|
||||||
|
|
||||||
|
if(meta != null) {
|
||||||
|
enchantmentLevels = itemStack.getItemMeta().getEnchants();
|
||||||
|
}
|
||||||
|
|
||||||
|
for(Map.Entry<Enchantment, Integer> entry : enchantmentLevels.entrySet()) {
|
||||||
|
if(conflictsWith(entry.getKey())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(itemStack.getItemMeta() instanceof EnchantmentStorageMeta esm) {
|
||||||
|
Map<Enchantment, Integer> storedEnchantmentLevels = esm.getStoredEnchants();
|
||||||
|
|
||||||
|
for(Map.Entry<Enchantment, Integer> 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<EquipmentSlot> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue