mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-19 14:59:21 +00:00
Compare commits
201 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c406f40898 | ||
|
|
7561762c25 | ||
|
|
d245245083 | ||
|
|
2b55e129b3 | ||
|
|
0caec74436 | ||
|
|
55e443cd49 | ||
|
|
b63e1bd283 | ||
|
|
575122e6dd | ||
|
|
856cbb9caa | ||
|
|
7034a97d3a | ||
|
|
635edb930f | ||
|
|
1d3e4b7a20 | ||
|
|
6f9abb63cc | ||
|
|
563b9e146e | ||
|
|
7c9ac37eb7 | ||
|
|
5d3de4115f | ||
|
|
105f65c93a | ||
|
|
9018ad02e1 | ||
|
|
9db9a3e721 | ||
|
|
9120c062de | ||
|
|
bd83c8935d | ||
|
|
62095364ce | ||
|
|
304df9984c | ||
|
|
b73de81519 | ||
|
|
12e882fe22 | ||
|
|
4ed8b94d55 | ||
|
|
854bf37186 | ||
|
|
8bab2a8123 | ||
|
|
97ad608d56 | ||
|
|
f7419f7277 | ||
|
|
f0497f61f0 | ||
|
|
f6aab54d4d | ||
|
|
c306d700ce | ||
|
|
bbcb091daf | ||
|
|
eb9e2491e5 | ||
|
|
0250ad80c8 | ||
|
|
516b163e07 | ||
|
|
c123b15708 | ||
|
|
29f8d8bf50 | ||
|
|
4f65cf49ef | ||
|
|
27f9e24dfb | ||
|
|
2cbe39f158 | ||
|
|
5840f61571 | ||
|
|
6bd12dc000 | ||
|
|
1afbd3753a | ||
|
|
38a063420b | ||
|
|
0fae3484a1 | ||
|
|
7bb4bff485 | ||
|
|
c5f5cd702e | ||
|
|
3ced2cc0af | ||
|
|
73e1840574 | ||
|
|
d1ca56af1f | ||
|
|
55211e30c5 | ||
|
|
2c1a38f2c9 | ||
|
|
0c7e052d44 | ||
|
|
54cc11fce0 | ||
|
|
3645fa01ec | ||
|
|
e20a0da845 | ||
|
|
7ed7d0a29e | ||
|
|
e7e7995e4e | ||
|
|
af57cfcf70 | ||
|
|
f1ac9b5e04 | ||
|
|
a9b1070725 | ||
|
|
5a000add98 | ||
|
|
aec2836d1e | ||
|
|
84e2ea3904 | ||
|
|
4f669170c2 | ||
|
|
8ea8c7b7ba | ||
|
|
acab4ae58a | ||
|
|
ea822b0f4b | ||
|
|
24a9974ff7 | ||
|
|
222a9871e0 | ||
|
|
0ce9d2ce74 | ||
|
|
cde500a123 | ||
|
|
f15790030f | ||
|
|
ce3350c6fa | ||
|
|
4288742052 | ||
|
|
8205b9c169 | ||
|
|
1d7f6a8d8b | ||
|
|
3425c97245 | ||
|
|
2d1d8f1ab6 | ||
|
|
f322d31b03 | ||
|
|
368e665ac3 | ||
|
|
922eb2f19a | ||
|
|
23e0123004 | ||
|
|
e98bac844a | ||
|
|
d6d9a55f72 | ||
|
|
e3070a65ab | ||
|
|
a8b4696604 | ||
|
|
7f5ca6206b | ||
|
|
ad885a9a15 | ||
|
|
fe89e7b770 | ||
|
|
17ea62ed0b | ||
|
|
94717637ba | ||
|
|
f6663f0c09 | ||
|
|
33588c2345 | ||
|
|
c2c5a424fb | ||
|
|
ce41053e87 | ||
|
|
5817de83e5 | ||
|
|
30dd48ce88 | ||
|
|
cf7912a89e | ||
|
|
9900b44858 | ||
|
|
9019181208 | ||
|
|
99483387f1 | ||
|
|
42177f2582 | ||
|
|
e4e0743205 | ||
|
|
105927a57f | ||
|
|
71706bf9ae | ||
|
|
101e0c11d7 | ||
|
|
70323fb2e2 | ||
|
|
9dc5577175 | ||
|
|
117d5edea2 | ||
|
|
3f0f518037 | ||
|
|
2017ecc20f | ||
|
|
ded89ad343 | ||
|
|
c4b194f8d6 | ||
|
|
d682e6e6c6 | ||
|
|
6fef9c4eae | ||
|
|
16eee05065 | ||
|
|
b664e2586d | ||
|
|
d594c9c257 | ||
|
|
532a65eca8 | ||
|
|
5af8ae0da5 | ||
|
|
c0709f82bd | ||
|
|
945b65e1bc | ||
|
|
efcb36d345 | ||
|
|
30cd89c578 | ||
|
|
bb3753b8e4 | ||
|
|
d5569ad3ed | ||
|
|
d8386fd2a2 | ||
|
|
3bfea58f35 | ||
|
|
51cf7beeb8 | ||
|
|
df247b41f4 | ||
|
|
bac760165e | ||
|
|
dd39482ed1 | ||
|
|
c05f165278 | ||
|
|
c888759d33 | ||
|
|
089ea5b63a | ||
|
|
9020e9d906 | ||
|
|
7584ea0070 | ||
|
|
9c243c2893 | ||
|
|
8ba90fadc4 | ||
|
|
480796fbee | ||
|
|
9b186ec97a | ||
|
|
d828631dea | ||
|
|
5b8de7967b | ||
|
|
4fddbc2b32 | ||
|
|
43cd367ca3 | ||
|
|
19ca504bab | ||
|
|
394b8ff1d1 | ||
|
|
4577da3336 | ||
|
|
c3b339b3dd | ||
|
|
2b91154ca2 | ||
|
|
2351be31e3 | ||
|
|
624543b93d | ||
|
|
c13f4b2a05 | ||
|
|
00b8d335d8 | ||
|
|
89d8b79ae3 | ||
|
|
acd97a1cb0 | ||
|
|
8c0f7a295f | ||
|
|
7536bfaaf5 | ||
|
|
cbf5d9c24e | ||
|
|
b9e474d946 | ||
|
|
3d232f97fb | ||
|
|
6d649d0889 | ||
|
|
0754837820 | ||
|
|
8f44dbb296 | ||
|
|
049cd8ecca | ||
|
|
2ed7705903 | ||
|
|
6bc6749e38 | ||
|
|
97a02b7a05 | ||
|
|
abc41a0aca | ||
|
|
31a14b2de7 | ||
|
|
59a0002c16 | ||
|
|
61020e04d9 | ||
|
|
ff1ace8342 | ||
|
|
847790c514 | ||
|
|
390a77b407 | ||
|
|
04ab9d14f8 | ||
|
|
3a32d481c4 | ||
|
|
8edbc029f8 | ||
|
|
258356e45d | ||
|
|
e1628b6448 | ||
|
|
fa32e97564 | ||
|
|
8080d57645 | ||
|
|
0a2f7b6cd4 | ||
|
|
26a2366876 | ||
|
|
2690ab3144 | ||
|
|
18b96944e9 | ||
|
|
3282f5739c | ||
|
|
50e66be0c0 | ||
|
|
593c88c8ba | ||
|
|
2f700b2d93 | ||
|
|
d1c95030f0 | ||
|
|
1ed2414241 | ||
|
|
8847483ff8 | ||
|
|
31552f85e4 | ||
|
|
125f142cf5 | ||
|
|
dc3882e47e | ||
|
|
dafbcad10e | ||
|
|
d1085ca7bd |
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Dependabot configuration file for GitHub
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gradle" # See documentation for possible values
|
||||
directory: "/" # Location of package manifests
|
||||
schedule:
|
||||
interval: "daily"
|
||||
3
.github/funding.yml
vendored
Normal file
3
.github/funding.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Funding metadata for GitHub
|
||||
github: WiIIiam278
|
||||
custom: https://buymeacoff.ee/william278
|
||||
37
.github/workflows/ci.yml
vendored
Normal file
37
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
# Builds, tests the project with Gradle
|
||||
name: CI Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
- 'README.md'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
SNAPSHOTS_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
SNAPSHOTS_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: Publish Test Report
|
||||
uses: mikepenz/action-junit-report@v3
|
||||
if: success() || failure() # always run even if the previous step fails
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
32
.github/workflows/java_ci.yml
vendored
32
.github/workflows/java_ci.yml
vendored
@@ -1,32 +0,0 @@
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
|
||||
|
||||
name: Java CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 16
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '16'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@67421db6bd0bf253fb4bd25b31ebb98943c375e1
|
||||
with:
|
||||
arguments: test
|
||||
24
.github/workflows/pr_tests.yml
vendored
Normal file
24
.github/workflows/pr_tests.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Carry out tests on pull requests
|
||||
name: PR Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ 'master' ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Test Pull Request
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: test
|
||||
33
.github/workflows/release.yml
vendored
Normal file
33
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
# Builds, tests and publishes to maven when a release is published
|
||||
name: Release Tests
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
- name: Build with Gradle
|
||||
uses: gradle/gradle-build-action@v2
|
||||
with:
|
||||
arguments: build test publish
|
||||
env:
|
||||
RELEASES_MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }}
|
||||
RELEASES_MAVEN_PASSWORD: ${{ secrets.MAVEN_PASSWORD }}
|
||||
- name: Publish Test Report
|
||||
uses: mikepenz/action-junit-report@v3
|
||||
if: success() || failure() # always run even if the previous step fails
|
||||
with:
|
||||
report_paths: '**/build/test-results/test/TEST-*.xml'
|
||||
28
.github/workflows/update_docs.yml
vendored
Normal file
28
.github/workflows/update_docs.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
# Update the GitHub Wiki documentation when a push is made to docs/
|
||||
name: Update Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master' ]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- 'workflows/**'
|
||||
tags-ignore:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
deploy-wiki:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Code'
|
||||
uses: actions/checkout@v3
|
||||
- name: 'Push Changes to Wiki'
|
||||
uses: Andrew-Chen-Wang/github-wiki-action@v3
|
||||
env:
|
||||
WIKI_DIR: 'docs/'
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
GH_MAIL: 'actions@github.com'
|
||||
GH_NAME: 'github-actions[bot]'
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -118,3 +118,8 @@ run/
|
||||
!gradle-wrapper.jar
|
||||
/build-output-final/
|
||||
/target/
|
||||
|
||||
# Don't include generated test suite files
|
||||
/test/servers/
|
||||
/test/HuskSync
|
||||
/test/config.yml
|
||||
16
HEADER
Normal file
16
HEADER
Normal file
@@ -0,0 +1,16 @@
|
||||
This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
|
||||
Copyright (c) William278 <will27528@gmail.com>
|
||||
Copyright (c) contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
217
LICENSE
217
LICENSE
@@ -1,21 +1,202 @@
|
||||
Copyright © William278 2022. All rights reserved
|
||||
|
||||
LICENSE
|
||||
This source code is provided as reference to licensed individuals that have purchased the HuskSync
|
||||
plugin once from any of the official sources it is provided. The availability of this code does
|
||||
not grant you the rights to modify, re-distribute, compile or redistribute this source code or
|
||||
"plugin" outside this intended purpose. This license does not cover libraries developed by third
|
||||
parties that are utilised in the plugin.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
CONTRIBUTOR AGREEMENT
|
||||
By contributing code to this repository, contributors agree that they forfeit their contributions
|
||||
to the copyright holder and only the copyright holder.
|
||||
In exchange for contributing, the copyright holder may give, at their discretion, permission to use
|
||||
the plugin in commercial contexts
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
DEFINITIONS
|
||||
"plugin"; the jar file compiled from this source code
|
||||
"source code"; the java source code and gradle configurations provided in this repository, however
|
||||
excludes libraries
|
||||
"copyright holder"; William278
|
||||
"contributor(s)"; person(s) who submit (contribute) code through a pull request to this repository
|
||||
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 saveCause 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 saveCause 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.
|
||||
104
README.md
104
README.md
@@ -1,61 +1,85 @@
|
||||
# [](https://github.com/WiIIiam278/HuskSync)
|
||||

|
||||
[](https://discord.gg/tVYhJfyDWG)
|
||||
<!--suppress ALL -->
|
||||
<p align="center">
|
||||
<img src="images/banner.png" alt="HuskSync" />
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml">
|
||||
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
|
||||
</a>
|
||||
<a href="https://jitpack.io/#net.william278/HuskSync">
|
||||
<img src="https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle" />
|
||||
</a>
|
||||
<a href="https://discord.gg/tVYhJfyDWG">
|
||||
<img src="https://img.shields.io/discord/818135932103557162.svg?label=&logo=discord&logoColor=fff&color=7389D8&labelColor=6A7EC2" />
|
||||
</a>
|
||||
<br/>
|
||||
<b>
|
||||
<a href="https://www.spigotmc.org/resources/husksync.97144/">Spigot</a>
|
||||
</b> —
|
||||
<b>
|
||||
<a href="https://william278.net/docs/husksync/setup">Setup</a>
|
||||
</b> —
|
||||
<b>
|
||||
<a href="https://william278.net/docs/husksync/">Docs</a>
|
||||
</b> —
|
||||
<b>
|
||||
<a href="https://github.com/WiIIiam278/HuskSync/issues">Issues</a>
|
||||
</b>
|
||||
</p>
|
||||
<br/>
|
||||
|
||||
[Documentation, Guides & API](https://william278.net/docs/husksync/Home) · [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) · [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
|
||||
**HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
||||
**HuskSync** is a modern, cross-server player data synchronization system that enables the comprehensive synchronization of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
||||
|
||||
## Features
|
||||
- Synchronise inventories, ender chests, advancements, statistics, experience points, health, max health, hunger, saturation, potion effects, persistent data container tags, game mode, location and more across multiple proxied servers.
|
||||
- Create and manage "snapshot" backups of user data and roll back users to previous states on-the-fly. (`/userdata`)
|
||||
- Preview, list, delete, restore & pin user data snapshots in-game with an intuitive menu.
|
||||
- Examine the contents of player's inventories and ender chests on-the-fly. (`/inventory`, `/enderchest`)
|
||||
- Hooks with your [Player Analytics](https://github.com/plan-player-analytics/Plan) web panel to provide an overview of user data.
|
||||
- Supports segregating synchronisation across multiple distinct clusters on one network.
|
||||
**⭐ Seamless synchronization** — Utilises optimised Redis caching when players change server to sync player data super quickly for a seamless experience.
|
||||
|
||||
## Requirements
|
||||
* A MySQL Database (v8.0+)
|
||||
* A Redis Database (v5.0+)
|
||||
* Any number of proxied Spigot servers (Minecraft v1.16.5+)
|
||||
**⭐ Complete player synchronization** — Sync inventories, Ender Chests, health, hunger, effects, advancements, statistics, locked maps & [more](https://william278.net/docs/husksync/sync-features)—no data left behind!
|
||||
|
||||
**⭐ Backup, restore & rotate** — Something gone wrong? Restore players back to a previous data state. Rotate and manage data snapshots in-game!
|
||||
|
||||
**⭐ Import existing data** — Import your MySQLPlayerDataBridge data—or from your existing world data! No server reset needed!
|
||||
|
||||
**⭐ Works great with Plan** — Stay in touch with your community through HuskSync analytics on your Plan web panel.
|
||||
|
||||
**⭐ Extensible API & open-source** — Need more? Extend the plugin with the Developer API. Or, submit a pull request through our code bounty system!
|
||||
|
||||
**Ready?** [It's syncing time!](https://william278.net/docs/husksync/setup)
|
||||
|
||||
## Setup
|
||||
1. Place the plugin jar file in the `/plugins/` directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) and fill in both the MySQL and Redis database credentials.
|
||||
4. Start every server again and synchronistaion will begin.
|
||||
Requires a MySQL (v8.0+) database, a Redis (v5.0+) server and any number of Spigot-based 1.16.5+ Minecraft servers, running Java 16+.
|
||||
|
||||
## Building
|
||||
1. Place the plugin jar file in the /plugins/ directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (~/plugins/HuskSync/config.yml) and fill in both the MySQL and Redis database credentials.
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
## Development
|
||||
To build HuskSync, simply run the following in the root of the repository:
|
||||
```
|
||||
|
||||
```bash
|
||||
./gradlew clean build
|
||||
```
|
||||
|
||||
## License
|
||||
HuskSync is a premium resource. This source code is provided as reference only for those who have purchased the resource from an official source.
|
||||
### License
|
||||
HuskSync is licensed under the Apache 2.0 license.
|
||||
|
||||
- [License](https://github.com/WiIIiam278/HuskSync/blob/master/LICENSE)
|
||||
|
||||
## Contributing
|
||||
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a license at my discretion to use HuskSync in commercial contexts without having to purchase the resource. Please read the information for contributors in the LICENSE file before submitting a pull request.
|
||||
Contributions to the project are welcome—feel free to open a pull request with new features, improvements and/or fixes!
|
||||
|
||||
## Translation
|
||||
### Support
|
||||
Due to its complexity, official support for HuskSync is provided through a paid model. This means that support is only available to users who have purchased a license to the plugin from Spigot, Polymart, or Craftaro and have provided proof of purchase. Please join our Discord server if you have done so and need help!
|
||||
|
||||
### Translations
|
||||
Translations of the plugin locales are welcome to help make the plugin more accessible. Please submit a pull request with your translations as a `.yml` file.
|
||||
|
||||
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales)
|
||||
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales/en-gb.yml)
|
||||
|
||||
## bStats
|
||||
This plugin uses bStats to provide me with metrics about its usage:
|
||||
- [bStats Metrics](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140)
|
||||
|
||||
You can turn metric collection off by navigating to `~/plugins/bStats/config.yml` and editing the config to disable plugin metrics.
|
||||
- [Locales Directory](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/languages)
|
||||
- [English Locales](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/languages/en-gb.yml)
|
||||
|
||||
## Links
|
||||
- [Documentation, Guides & API](https://william278.net/docs/husksync/Home)
|
||||
- [Resource Page](https://www.spigotmc.org/resources/husksync.97144/)
|
||||
- [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
- [Discord Support](https://discord.gg/tVYhJfyDWG) (Proof of purchase required)
|
||||
- [Docs](https://william278.net/docs/husksync/) — Read the plugin documentation!
|
||||
- [Spigot](https://www.spigotmc.org/resources/husksync.97144/) — View the Spigot resource page (Also: [Polymart](https://polymart.org/resource/husksync.1634), [Craftaro](https://craftaro.com/marketplace/product/husksync.758))
|
||||
- [Issues](https://github.com/WiIIiam278/HuskSync/issues) — File a bug report or feature request
|
||||
- [Discord](https://discord.gg/tVYhJfyDWG) — Get help, ask questions (Purchase required)
|
||||
- [bStats](https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140) — View plugin metrics
|
||||
|
||||
---
|
||||
© [William278](https://william278.net/), 2022. All rights reserved.
|
||||
© [William278](https://william278.net/), 2023. Licensed under the Apache-2.0 License.
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
dependencies {
|
||||
implementation project(path: ':bukkit')
|
||||
compileOnly project(path: ':common')
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The HuskSync API implementation for the Bukkit platform, providing methods to access and modify player {@link UserData} held by {@link User}s.
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the API class
|
||||
*/
|
||||
private static final HuskSyncAPI INSTANCE = new HuskSyncAPI();
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API
|
||||
*/
|
||||
private HuskSyncAPI() {
|
||||
super(BukkitHuskSync.getInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
*/
|
||||
public static @NotNull HuskSyncAPI getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
* @param player the bukkit player to get the {@link User} instance for
|
||||
* @return the {@link User} instance for the given bukkit {@link Player}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public OnlineUser getUser(@NotNull Player player) {
|
||||
return BukkitPlayer.adapt(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory in the database of the given {@link User} to the given {@link ItemStack} contents
|
||||
*
|
||||
* @param user the {@link User} to set the inventory of
|
||||
* @param inventoryContents the {@link ItemStack} contents to set the inventory to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull ItemStack[] inventoryContents) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(inventoryContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getInventoryData().serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory in the database of the given {@link User} to the given {@link BukkitInventoryMap} contents
|
||||
*
|
||||
* @param user the {@link User} to set the inventory of
|
||||
* @param inventoryMap the {@link BukkitInventoryMap} contents to set the inventory to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setInventoryData(@NotNull User user, @NotNull BukkitInventoryMap inventoryMap) {
|
||||
return setInventoryData(user, inventoryMap.getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Ender Chest in the database of the given {@link User} to the given {@link ItemStack} contents
|
||||
*
|
||||
* @param user the {@link User} to set the Ender Chest of
|
||||
* @param enderChestContents the {@link ItemStack} contents to set the Ender Chest to
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Void> setEnderChestData(@NotNull User user, @NotNull ItemStack[] enderChestContents) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(enderChestContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getEnderChestData().serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link BukkitInventoryMap} for the given {@link User}, containing their current inventory item data
|
||||
*
|
||||
* @param user the {@link User} to get the {@link BukkitInventoryMap} for
|
||||
* @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeInventory(userData
|
||||
.getInventoryData().serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link ItemStack}s array contents of the given {@link User}'s Ender Chest data
|
||||
*
|
||||
* @param user the {@link User} to get the Ender Chest contents of
|
||||
* @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeItemStackArray(userData
|
||||
.getEnderChestData().serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a Base-64 encoded inventory array string into a {@link ItemStack} array.
|
||||
*
|
||||
* @param serializedItemStackArray The Base-64 encoded inventory array string.
|
||||
* @return The deserialized {@link ItemStack} array.
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializedItemStackArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializeItemStackArray(serializedItemStackArray).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a serialized {@link ItemStack} array of player inventory contents into a {@link BukkitInventoryMap}
|
||||
*
|
||||
* @param serializedInventory The serialized {@link ItemStack} array of player inventory contents.
|
||||
* @return A {@link BukkitInventoryMap} of the deserialized {@link ItemStack} contents array
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedInventory)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializeInventory(serializedInventory).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link ItemStack} array into a Base-64 encoded string.
|
||||
*
|
||||
* @param itemStacks The {@link ItemStack} array to serialize.
|
||||
* @return The serialized Base-64 encoded string.
|
||||
* @throws DataSerializationException If an error occurs during serialization.
|
||||
* @see #deserializeItemStackArray(String)
|
||||
* @see ItemData
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] itemStacks)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializeItemStackArray(itemStacks).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a Base-64 encoded potion effect array string into a {@link PotionEffect} array.
|
||||
*
|
||||
* @param serializedPotionEffectArray The Base-64 encoded potion effect array string.
|
||||
* @return The deserialized {@link PotionEffect} array.
|
||||
* @throws DataSerializationException If an error occurs during deserialization.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String serializedPotionEffectArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer
|
||||
.deserializePotionEffectArray(serializedPotionEffectArray).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a {@link PotionEffect} array into a Base-64 encoded string.
|
||||
*
|
||||
* @param potionEffects The {@link PotionEffect} array to serialize.
|
||||
* @return The serialized Base-64 encoded string.
|
||||
* @throws DataSerializationException If an error occurs during serialization.
|
||||
* @see #deserializePotionEffectArray(String)
|
||||
* @see PotionEffectData
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> BukkitSerializer.serializePotionEffectArray(potionEffects).join());
|
||||
}
|
||||
|
||||
}
|
||||
83
build.gradle
83
build.gradle
@@ -1,21 +1,31 @@
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
||||
id 'org.ajoberstar.grgit' version '5.0.0'
|
||||
id 'java'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1'
|
||||
id 'org.cadixdev.licenser' version '0.6.1' apply false
|
||||
id 'org.ajoberstar.grgit' version '5.2.0'
|
||||
id 'maven-publish'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group 'net.william278'
|
||||
version "$ext.plugin_version+${versionMetadata()}"
|
||||
version "$ext.plugin_version${versionMetadata()}"
|
||||
description "$ext.plugin_description"
|
||||
defaultTasks 'licenseFormat', 'build'
|
||||
|
||||
ext {
|
||||
set 'version', version.toString()
|
||||
set 'description', description.toString()
|
||||
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'mariadb_driver_version', mariadb_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
|
||||
allprojects {
|
||||
apply plugin: 'com.github.johnrengelman.shadow'
|
||||
apply plugin: 'org.cadixdev.licenser'
|
||||
apply plugin: 'java'
|
||||
|
||||
compileJava.options.encoding = 'UTF-8'
|
||||
@@ -28,20 +38,31 @@ allprojects {
|
||||
mavenLocal()
|
||||
mavenCentral()
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url 'https://repo.codemc.io/repository/maven-public/' }
|
||||
maven { url 'https://repo.minebench.de/' }
|
||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://mvn-repo.arim.space/lesser-gpl3/' }
|
||||
maven { url 'https://libraries.minecraft.net/' }
|
||||
maven { url 'https://repo.william278.net/releases/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.0'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
license {
|
||||
header = rootProject.file('HEADER')
|
||||
include '**/*.java'
|
||||
newLine = true
|
||||
}
|
||||
|
||||
processResources {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: rootProject.ext.properties
|
||||
@@ -52,14 +73,18 @@ subprojects {
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
||||
|
||||
if (['bukkit', 'api', 'plugin'].contains(project.name)) {
|
||||
jar {
|
||||
from '../LICENSE'
|
||||
}
|
||||
|
||||
if (['bukkit', 'plugin'].contains(project.name)) {
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if ('api'.contains(project.name)) {
|
||||
if ('bukkit'.contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
@@ -73,6 +98,35 @@ subprojects {
|
||||
shadowJar.dependsOn(sourcesJar, javadocJar)
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
if (System.getenv("RELEASES_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-releases"
|
||||
url = "https://repo.william278.net/releases"
|
||||
credentials {
|
||||
username = System.getenv("RELEASES_MAVEN_USERNAME")
|
||||
password = System.getenv("RELEASES_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (System.getenv("SNAPSHOTS_MAVEN_USERNAME") != null) {
|
||||
maven {
|
||||
name = "william278-snapshots"
|
||||
url = "https://repo.william278.net/snapshots"
|
||||
credentials {
|
||||
username = System.getenv("SNAPSHOTS_MAVEN_USERNAME")
|
||||
password = System.getenv("SNAPSHOTS_MAVEN_PASSWORD")
|
||||
}
|
||||
authentication {
|
||||
basic(BasicAuthentication)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
groupId = 'net.william278'
|
||||
@@ -95,8 +149,15 @@ logger.lifecycle("Building HuskSync ${version} by William278")
|
||||
|
||||
@SuppressWarnings('GrMethodMayBeStatic')
|
||||
def versionMetadata() {
|
||||
if (grgit == null) {
|
||||
return System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
||||
// Get if there is a tag for this commit
|
||||
def tag = grgit.tag.list().find { it.commit.id == grgit.head().id }
|
||||
if (tag != null) {
|
||||
return ''
|
||||
}
|
||||
return 'rev.' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
|
||||
// Otherwise, get the last commit hash and if it's a clean head
|
||||
if (grgit == null) {
|
||||
return '-' + System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
||||
}
|
||||
return '-' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
}
|
||||
@@ -1,29 +1,60 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.0'
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.2'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
||||
implementation 'net.william278:hsldataconverter:1.0'
|
||||
implementation 'net.william278:mapdataapi:1.0.3'
|
||||
implementation 'net.william278:andjam:1.0.2'
|
||||
implementation 'me.lucko:commodore:2.2'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.3.0'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.5'
|
||||
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
|
||||
implementation 'de.tr7zw:item-nbt-api:2.12.0'
|
||||
|
||||
compileOnly 'redis.clients:jedis:4.2.3'
|
||||
compileOnly 'commons-io:commons-io:2.11.0'
|
||||
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'commons-io:commons-io:2.13.0'
|
||||
compileOnly 'org.json:json:20230618'
|
||||
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
compileOnly 'com.zaxxer:HikariCP:5.0.1'
|
||||
compileOnly 'net.william278:DesertWell:2.0.4'
|
||||
compileOnly 'net.william278:annotaml:2.0.7'
|
||||
compileOnly 'net.william278:AdvancementAPI:97a9583413'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
dependencies {
|
||||
exclude(dependency('com.mojang:brigadier'))
|
||||
}
|
||||
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'org.apache.commons.text', 'net.william278.husksync.libraries.commons.text'
|
||||
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.kyori', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
|
||||
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
|
||||
relocate 'net.william278.mapdataapi', 'net.william278.husksync.libraries.mapdataapi'
|
||||
relocate 'net.william278.andjam', 'net.william278.husksync.libraries.andjam'
|
||||
relocate 'net.querz', 'net.william278.husksync.libraries.nbtparser'
|
||||
relocate 'net.roxeez', 'net.william278.husksync.libraries'
|
||||
|
||||
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
|
||||
relocate 'space.arim.morepaperlib', 'net.william278.husksync.libraries.paperlib'
|
||||
relocate 'de.tr7zw.changeme.nbtapi', 'net.william278.husksync.libraries.nbtapi'
|
||||
}
|
||||
@@ -1,250 +1,233 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning;
|
||||
import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.general.GeneralSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings;
|
||||
import com.google.gson.Gson;
|
||||
import net.kyori.adventure.platform.bukkit.BukkitAudiences;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.adapter.GsonAdapter;
|
||||
import net.william278.husksync.adapter.SnappyGsonAdapter;
|
||||
import net.william278.husksync.api.BukkitHuskSyncAPI;
|
||||
import net.william278.husksync.command.BukkitCommand;
|
||||
import net.william278.husksync.command.BukkitCommandType;
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.CompressedDataAdapter;
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.data.JsonDataAdapter;
|
||||
import net.william278.husksync.data.BukkitSerializer;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.editor.DataEditor;
|
||||
import net.william278.husksync.event.BukkitEventCannon;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.event.BukkitEventDispatcher;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.migrator.LegacyMigrator;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.migrator.MpdbMigrator;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.*;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
import net.william278.husksync.util.BukkitMapPersister;
|
||||
import net.william278.husksync.util.BukkitTask;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.bukkit.map.MapView;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.MorePaperLib;
|
||||
import space.arim.morepaperlib.commands.CommandRegistration;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.GracefulScheduling;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.Supplier, BukkitEventDispatcher,
|
||||
BukkitMapPersister {
|
||||
|
||||
/**
|
||||
* Metrics ID for <a href="https://bstats.org/plugin/bukkit/HuskSync%20-%20Bukkit/13140">HuskSync on Bukkit</a>.
|
||||
*/
|
||||
private static final int METRICS_ID = 13140;
|
||||
private static final String PLATFORM_TYPE_ID = "bukkit";
|
||||
|
||||
private Database database;
|
||||
private RedisManager redisManager;
|
||||
private Logger logger;
|
||||
private ResourceReader resourceReader;
|
||||
private EventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private DataEditor dataEditor;
|
||||
private EventCannon eventCannon;
|
||||
private Map<Identifier, Serializer<? extends Data>> serializers;
|
||||
private Map<UUID, Map<Identifier, Data>> playerCustomDataStore;
|
||||
private Settings settings;
|
||||
private Locales locales;
|
||||
private List<Migrator> availableMigrators;
|
||||
private static BukkitHuskSync instance;
|
||||
|
||||
/**
|
||||
* (<b>Internal use only)</b> Returns the instance of the implementing Bukkit plugin
|
||||
*
|
||||
* @return the instance of the Bukkit plugin
|
||||
*/
|
||||
public static BukkitHuskSync getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
}
|
||||
private LegacyConverter legacyConverter;
|
||||
private Map<Integer, MapView> mapViews;
|
||||
private BukkitAudiences audiences;
|
||||
private MorePaperLib paperLib;
|
||||
private AsynchronousScheduler asyncScheduler;
|
||||
private RegionalScheduler regionalScheduler;
|
||||
private Gson gson;
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Initialize HuskSync
|
||||
final AtomicBoolean initialized = new AtomicBoolean(true);
|
||||
try {
|
||||
// Set the logging adapter and resource reader
|
||||
this.logger = new BukkitLogger(this.getLogger());
|
||||
this.resourceReader = new BukkitResourceReader(this);
|
||||
// Initial plugin setup
|
||||
this.gson = createGson();
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
this.paperLib = new MorePaperLib(this);
|
||||
this.availableMigrators = new ArrayList<>();
|
||||
this.serializers = new LinkedHashMap<>();
|
||||
this.playerCustomDataStore = new ConcurrentHashMap<>();
|
||||
this.mapViews = new ConcurrentHashMap<>();
|
||||
|
||||
// Load settings and locales
|
||||
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales...");
|
||||
initialized.set(reload().join());
|
||||
if (initialized.get()) {
|
||||
logger.showDebugLogs(settings.getBooleanValue(Settings.ConfigOption.DEBUG_LOGGING));
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
|
||||
// Load settings and locales
|
||||
initialize("plugin config & locale files", (plugin) -> this.loadConfigs());
|
||||
|
||||
// Prepare data adapter
|
||||
initialize("data adapter", (plugin) -> {
|
||||
if (settings.doCompressData()) {
|
||||
dataAdapter = new SnappyGsonAdapter(this);
|
||||
} else {
|
||||
throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
|
||||
dataAdapter = new GsonAdapter(this);
|
||||
}
|
||||
});
|
||||
|
||||
// Prepare data adapter
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) {
|
||||
dataAdapter = new CompressedDataAdapter();
|
||||
} else {
|
||||
dataAdapter = new JsonDataAdapter();
|
||||
}
|
||||
// Prepare serializers
|
||||
initialize("data serializers", (plugin) -> {
|
||||
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
|
||||
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
|
||||
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
|
||||
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Location(this));
|
||||
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Health(this));
|
||||
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Hunger(this));
|
||||
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.GameMode(this));
|
||||
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
|
||||
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Statistics(this));
|
||||
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Experience(this));
|
||||
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
|
||||
});
|
||||
|
||||
// Prepare event cannon
|
||||
eventCannon = new BukkitEventCannon();
|
||||
|
||||
// Prepare data editor
|
||||
dataEditor = new DataEditor(locales);
|
||||
|
||||
// Prepare migrators
|
||||
availableMigrators = new ArrayList<>();
|
||||
// Setup available migrators
|
||||
initialize("data migrators/converters", (plugin) -> {
|
||||
availableMigrators.add(new LegacyMigrator(this));
|
||||
final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
if (mySqlPlayerDataBridge != null) {
|
||||
availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
|
||||
if (isDependencyLoaded("MySqlPlayerDataBridge")) {
|
||||
availableMigrators.add(new MpdbMigrator(this));
|
||||
}
|
||||
legacyConverter = new BukkitLegacyConverter(this);
|
||||
});
|
||||
|
||||
// Prepare database connection
|
||||
this.database = new MySqlDatabase(settings, resourceReader, logger, dataAdapter, eventCannon);
|
||||
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database...");
|
||||
initialized.set(this.database.initialize());
|
||||
if (initialized.get()) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the database");
|
||||
} else {
|
||||
throw new HuskSyncInitializationException("Failed to establish a connection to the database. " +
|
||||
"Please check the supplied database credentials in the config file");
|
||||
}
|
||||
// Initialize the database
|
||||
initialize(getSettings().getDatabaseType().getDisplayName() + " database connection", (plugin) -> {
|
||||
this.database = new MySqlDatabase(this);
|
||||
this.database.initialize();
|
||||
});
|
||||
|
||||
// Prepare redis connection
|
||||
// Prepare redis connection
|
||||
initialize("Redis server connection", (plugin) -> {
|
||||
this.redisManager = new RedisManager(this);
|
||||
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
|
||||
initialized.set(this.redisManager.initialize().join());
|
||||
if (initialized.get()) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
|
||||
} else {
|
||||
throw new HuskSyncInitializationException("Failed to establish a connection to the Redis server. " +
|
||||
"Please check the supplied Redis credentials in the config file");
|
||||
}
|
||||
this.redisManager.initialize();
|
||||
});
|
||||
|
||||
// Register events
|
||||
getLoggingAdapter().log(Level.INFO, "Registering events...");
|
||||
this.eventListener = new BukkitEventListener(this);
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully registered events listener");
|
||||
// Register events
|
||||
initialize("events", (plugin) -> this.eventListener = new BukkitEventListener(this));
|
||||
|
||||
// Register permissions
|
||||
getLoggingAdapter().log(Level.INFO, "Registering permissions & commands...");
|
||||
Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager()
|
||||
.addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) {
|
||||
case EVERYONE -> PermissionDefault.TRUE;
|
||||
case NOBODY -> PermissionDefault.FALSE;
|
||||
case OPERATORS -> PermissionDefault.OP;
|
||||
})));
|
||||
// Register commands
|
||||
initialize("commands", (plugin) -> BukkitCommand.Type.registerCommands(this));
|
||||
|
||||
// Register commands
|
||||
for (final BukkitCommandType bukkitCommandType : BukkitCommandType.values()) {
|
||||
final PluginCommand pluginCommand = getCommand(bukkitCommandType.commandBase.command);
|
||||
if (pluginCommand != null) {
|
||||
new BukkitCommand(bukkitCommandType.commandBase, this).register(pluginCommand);
|
||||
}
|
||||
// Register plugin hooks
|
||||
initialize("hooks", (plugin) -> {
|
||||
if (isDependencyLoaded("Plan") && getSettings().usePlanHook()) {
|
||||
new PlanHook(this).hookIntoPlan();
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands");
|
||||
});
|
||||
|
||||
// Hook into plan
|
||||
if (Bukkit.getPluginManager().getPlugin("Plan") != null) {
|
||||
getLoggingAdapter().log(Level.INFO, "Enabling Plan integration...");
|
||||
new PlanHook(database, logger).hookIntoPlan();
|
||||
getLoggingAdapter().log(Level.INFO, "Plan integration enabled!");
|
||||
}
|
||||
// Register API
|
||||
initialize("api", (plugin) -> BukkitHuskSyncAPI.register(this));
|
||||
|
||||
// Hook into bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (final Exception e) {
|
||||
getLoggingAdapter().log(Level.WARNING, "Skipped bStats metrics initialization due to an exception.");
|
||||
}
|
||||
|
||||
// Check for updates
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
|
||||
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
|
||||
CompletableFuture.runAsync(() -> new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole());
|
||||
}
|
||||
} catch (HuskSyncInitializationException exception) {
|
||||
getLoggingAdapter().log(Level.SEVERE, exception.getMessage());
|
||||
initialized.set(false);
|
||||
} catch (Exception exception) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
|
||||
initialized.set(false);
|
||||
} finally {
|
||||
// Validate initialization
|
||||
if (initialized.get()) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion());
|
||||
} else {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
}
|
||||
}
|
||||
// Hook into bStats and check for updates
|
||||
initialize("metrics", (plugin) -> this.registerMetrics(METRICS_ID));
|
||||
this.checkForUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Handle shutdown
|
||||
if (this.eventListener != null) {
|
||||
this.eventListener.handlePluginDisable();
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
|
||||
|
||||
// Unregister API and cancel tasks
|
||||
BukkitHuskSyncAPI.unregister();
|
||||
this.cancelTasks();
|
||||
|
||||
// Complete shutdown
|
||||
log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<OnlineUser> getOnlineUsers() {
|
||||
return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet());
|
||||
@NotNull
|
||||
public Set<OnlineUser> getOnlineUsers() {
|
||||
return Bukkit.getOnlinePlayers().stream()
|
||||
.map(player -> BukkitUser.adapt(player, this))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
@NotNull
|
||||
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
final Player player = Bukkit.getPlayer(uuid);
|
||||
if (player == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(BukkitPlayer.adapt(player));
|
||||
return Optional.of(BukkitUser.adapt(player, this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Database getDatabase() {
|
||||
@NotNull
|
||||
public Database getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull RedisManager getRedisManager() {
|
||||
@NotNull
|
||||
public RedisManager getRedisManager() {
|
||||
return redisManager;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public @NotNull DataAdapter getDataAdapter() {
|
||||
public DataAdapter getDataAdapter() {
|
||||
return dataAdapter;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public @NotNull DataEditor getDataEditor() {
|
||||
return dataEditor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EventCannon getEventCannon() {
|
||||
return eventCannon;
|
||||
public Map<Identifier, Serializer<? extends Data>> getSerializers() {
|
||||
return serializers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -253,43 +236,143 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
return availableMigrators;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public @NotNull Settings getSettings() {
|
||||
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
|
||||
if (playerCustomDataStore.containsKey(user.getUuid())) {
|
||||
return playerCustomDataStore.get(user.getUuid());
|
||||
}
|
||||
final Map<Identifier, Data> data = new HashMap<>();
|
||||
playerCustomDataStore.put(user.getUuid(), data);
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Settings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Locales getLocales() {
|
||||
public void setSettings(@NotNull Settings settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public Locales getLocales() {
|
||||
return locales;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Logger getLoggingAdapter() {
|
||||
return logger;
|
||||
public void setLocales(@NotNull Locales locales) {
|
||||
this.locales = locales;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Version getPluginVersion() {
|
||||
return Version.pluginVersion(getDescription().getVersion());
|
||||
public boolean isDependencyLoaded(@NotNull String name) {
|
||||
return Bukkit.getPluginManager().getPlugin(name) != null;
|
||||
}
|
||||
|
||||
// Register bStats metrics
|
||||
public void registerMetrics(int metricsId) {
|
||||
if (!getPluginVersion().getMetadata().isBlank()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
new Metrics(this, metricsId);
|
||||
} catch (Throwable e) {
|
||||
log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||
public void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable) {
|
||||
if (throwable.length > 0) {
|
||||
getLogger().log(level, message, throwable[0]);
|
||||
} else {
|
||||
getLogger().log(level, message);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public ConsoleUser getConsole() {
|
||||
return new ConsoleUser(audiences.console());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getPluginVersion() {
|
||||
return Version.fromString(getDescription().getVersion(), "-");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getPlatformType() {
|
||||
return PLATFORM_TYPE_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> reload() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
this.settings = Settings.load(YamlDocument.create(new File(getDataFolder(), "config.yml"), Objects.requireNonNull(resourceReader.getResource("config.yml")), GeneralSettings.builder().setUseDefaults(false).build(), LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.builder().setEncoding(DumperSettings.Encoding.UNICODE).build(), UpdaterSettings.builder().setVersioning(new BasicVersioning("config_version")).build()));
|
||||
|
||||
this.locales = Locales.load(YamlDocument.create(new File(getDataFolder(), "messages-" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"), Objects.requireNonNull(resourceReader.getResource("locales/" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"))));
|
||||
return true;
|
||||
} catch (IOException | NullPointerException e) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
public Optional<LegacyConverter> getLegacyConverter() {
|
||||
return Optional.of(legacyConverter);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Set<UUID> getLockedPlayers() {
|
||||
return this.eventListener.getLockedPlayers();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Gson getGson() {
|
||||
return gson;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<Integer, MapView> getMapViews() {
|
||||
return mapViews;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public GracefulScheduling getScheduler() {
|
||||
return paperLib.scheduling();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public AsynchronousScheduler getAsyncScheduler() {
|
||||
return asyncScheduler == null
|
||||
? asyncScheduler = getScheduler().asyncScheduler() : asyncScheduler;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public RegionalScheduler getRegionalScheduler() {
|
||||
return regionalScheduler == null
|
||||
? regionalScheduler = getScheduler().globalRegionalScheduler() : regionalScheduler;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public BukkitAudiences getAudiences() {
|
||||
return audiences;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public CommandRegistration getCommandRegistrar() {
|
||||
return paperLib.commandRegistration();
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public HuskSync getPlugin() {
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.DataHolder;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* The HuskSync API implementation for the Bukkit platform
|
||||
* </p>
|
||||
* Retrieve an instance of the API class via {@link #getInstance()}.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitHuskSyncAPI extends HuskSyncAPI {
|
||||
|
||||
// Instance of the plugin
|
||||
private static BukkitHuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the API.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
private BukkitHuskSyncAPI(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entrypoint to the HuskSync API - returns an instance of the API
|
||||
*
|
||||
* @return instance of the HuskSync API
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static BukkitHuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
throw new NotRegisteredException();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Register the API for this platform.
|
||||
*
|
||||
* @param plugin the plugin instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void register(@NotNull BukkitHuskSync plugin) {
|
||||
instance = new BukkitHuskSyncAPI(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Unregister the API for this platform.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public static void unregister() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link OnlineUser} instance for the given bukkit {@link Player}.
|
||||
*
|
||||
* @param player the bukkit player to get the {@link OnlineUser} instance for
|
||||
* @return the {@link OnlineUser} instance for the given bukkit {@link Player}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitUser getUser(@NotNull Player player) {
|
||||
return BukkitUser.adapt(player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to get the inventory of
|
||||
* @return the {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitData.Items.Inventory>> getCurrentInventory(@NotNull User user) {
|
||||
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getInventory)
|
||||
.map(BukkitData.Items.Inventory.class::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.Inventory} of the given {@link Player}
|
||||
*
|
||||
* @param user the user to get the inventory of
|
||||
* @return the {@link BukkitData.Items.Inventory} of the given {@link Player}
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getCurrentInventoryContents(@NotNull User user) {
|
||||
return getCurrentInventory(user)
|
||||
.thenApply(inventory -> inventory.map(BukkitData.Items.Inventory::getContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the inventory of
|
||||
* @param contents the contents to set the inventory to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentInventory(@NotNull User user, @NotNull BukkitData.Items.Inventory contents) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.setInventory(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the inventory of
|
||||
* @param contents the contents to set the inventory to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentInventoryContents(@NotNull User user, @NotNull ItemStack[] contents) {
|
||||
editCurrentData(
|
||||
user,
|
||||
dataHolder -> dataHolder.getInventory().ifPresent(
|
||||
inv -> inv.setContents(adaptItems(contents))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the inventory of
|
||||
* @param editor the editor to apply to the inventory
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentInventory(@NotNull User user, ThrowingConsumer<BukkitData.Items.Inventory> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getInventory()
|
||||
.map(BukkitData.Items.Inventory.class::cast)
|
||||
.ifPresent(editor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.Inventory} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the inventory of
|
||||
* @param editor the editor to apply to the inventory
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentInventoryContents(@NotNull User user, ThrowingConsumer<ItemStack[]> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getInventory()
|
||||
.map(BukkitData.Items.Inventory.class::cast)
|
||||
.ifPresent(inventory -> editor.accept(inventory.getContents())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to get the ender chest of
|
||||
* @return the {@link BukkitData.Items.EnderChest} of the given {@link User}, or {@link Optional#empty()} if the
|
||||
* user data could not be found
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitData.Items.EnderChest>> getCurrentEnderChest(@NotNull User user) {
|
||||
return getCurrentData(user).thenApply(data -> data.flatMap(DataHolder::getEnderChest)
|
||||
.map(BukkitData.Items.EnderChest.class::cast));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current {@link BukkitData.Items.EnderChest} of the given {@link Player}
|
||||
*
|
||||
* @param user the user to get the ender chest of
|
||||
* @return the {@link BukkitData.Items.EnderChest} of the given {@link Player}, or {@link Optional#empty()} if the
|
||||
* user data could not be found
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getCurrentEnderChestContents(@NotNull User user) {
|
||||
return getCurrentEnderChest(user)
|
||||
.thenApply(enderChest -> enderChest.map(BukkitData.Items.EnderChest::getContents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the ender chest of
|
||||
* @param contents the contents to set the ender chest to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentEnderChest(@NotNull User user, @NotNull BukkitData.Items.EnderChest contents) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.setEnderChest(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to set the ender chest of
|
||||
* @param contents the contents to set the ender chest to
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentEnderChestContents(@NotNull User user, @NotNull ItemStack[] contents) {
|
||||
editCurrentData(
|
||||
user,
|
||||
dataHolder -> dataHolder.getEnderChest().ifPresent(
|
||||
enderChest -> enderChest.setContents(adaptItems(contents))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the ender chest of
|
||||
* @param editor the editor to apply to the ender chest
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentEnderChest(@NotNull User user, Consumer<BukkitData.Items.EnderChest> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
|
||||
.map(BukkitData.Items.EnderChest.class::cast)
|
||||
.ifPresent(editor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the current {@link BukkitData.Items.EnderChest} of the given {@link User}
|
||||
*
|
||||
* @param user the user to edit the ender chest of
|
||||
* @param editor the editor to apply to the ender chest
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentEnderChestContents(@NotNull User user, Consumer<ItemStack[]> editor) {
|
||||
editCurrentData(user, dataHolder -> dataHolder.getEnderChest()
|
||||
.map(BukkitData.Items.EnderChest.class::cast)
|
||||
.ifPresent(enderChest -> editor.accept(enderChest.getContents())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts an array of {@link ItemStack} to a {@link BukkitData.Items} instance
|
||||
*
|
||||
* @param contents the contents to adapt
|
||||
* @return the adapted {@link BukkitData.Items} instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitData.Items adaptItems(@NotNull ItemStack[] contents) {
|
||||
return BukkitData.Items.ItemArray.adapt(contents);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import me.lucko.commodore.file.CommodoreFileReader;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BrigadierUtil {
|
||||
|
||||
/**
|
||||
* Uses commodore to register command completions.
|
||||
*
|
||||
* @param plugin instance of the registering Bukkit plugin
|
||||
* @param bukkitCommand the Bukkit PluginCommand to register completions for
|
||||
* @param command the {@link Command} to register completions for
|
||||
*/
|
||||
protected static void registerCommodore(@NotNull BukkitHuskSync plugin,
|
||||
@NotNull org.bukkit.command.Command bukkitCommand,
|
||||
@NotNull Command command) {
|
||||
final InputStream commodoreFile = plugin.getResource(
|
||||
"commodore/" + bukkitCommand.getName() + ".commodore"
|
||||
);
|
||||
if (commodoreFile == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
CommodoreProvider.getCommodore(plugin).register(bukkitCommand,
|
||||
CommodoreFileReader.INSTANCE.parse(commodoreFile),
|
||||
player -> player.hasPermission(command.getPermission()));
|
||||
} catch (IOException e) {
|
||||
plugin.log(Level.SEVERE, String.format(
|
||||
"Failed to read command commodore completions for %s", bukkitCommand.getName()), e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +1,164 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import org.bukkit.command.*;
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Bukkit executor that implements and executes {@link CommandBase}s
|
||||
*/
|
||||
public class BukkitCommand implements CommandExecutor, TabExecutor {
|
||||
public class BukkitCommand extends org.bukkit.command.Command {
|
||||
|
||||
/**
|
||||
* The {@link CommandBase} that will be executed
|
||||
*/
|
||||
private final CommandBase command;
|
||||
private final BukkitHuskSync plugin;
|
||||
private final Command command;
|
||||
|
||||
/**
|
||||
* The implementing plugin
|
||||
*/
|
||||
private final HuskSync plugin;
|
||||
|
||||
public BukkitCommand(@NotNull CommandBase command, @NotNull HuskSync implementor) {
|
||||
public BukkitCommand(@NotNull Command command, @NotNull BukkitHuskSync plugin) {
|
||||
super(command.getName(), command.getDescription(), command.getUsage(), command.getAliases());
|
||||
this.command = command;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link PluginCommand} to this implementation
|
||||
*
|
||||
* @param pluginCommand {@link PluginCommand} to register
|
||||
*/
|
||||
public void register(@NotNull PluginCommand pluginCommand) {
|
||||
pluginCommand.setExecutor(this);
|
||||
pluginCommand.setTabCompleter(this);
|
||||
pluginCommand.setPermission(command.permission);
|
||||
pluginCommand.setDescription(command.getDescription());
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
if (sender instanceof Player player) {
|
||||
this.command.onExecute(BukkitPlayer.adapt(player), args);
|
||||
} else {
|
||||
if (this.command instanceof ConsoleExecutable consoleExecutable) {
|
||||
consoleExecutable.onConsoleExecute(args);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only").
|
||||
ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent()));
|
||||
}
|
||||
}
|
||||
public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) {
|
||||
this.command.onExecuted(sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole(), args);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@NotNull
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
if (this.command instanceof TabCompletable tabCompletable) {
|
||||
return tabCompletable.onTabComplete(args);
|
||||
public List<String> tabComplete(@NotNull CommandSender sender, @NotNull String alias,
|
||||
@NotNull String[] args) throws IllegalArgumentException {
|
||||
if (!(this.command instanceof TabProvider provider)) {
|
||||
return List.of();
|
||||
}
|
||||
return Collections.emptyList();
|
||||
final CommandUser user = sender instanceof Player p ? BukkitUser.adapt(p, plugin) : plugin.getConsole();
|
||||
if (getPermission() == null || user.hasPermission(getPermission())) {
|
||||
return provider.getSuggestions(user, args);
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
public void register() {
|
||||
// Register with bukkit
|
||||
plugin.getCommandRegistrar().getServerCommandMap().register("husksync", this);
|
||||
|
||||
// Register permissions
|
||||
BukkitCommand.addPermission(
|
||||
plugin,
|
||||
command.getPermission(),
|
||||
command.getUsage(),
|
||||
BukkitCommand.getPermissionDefault(command.isOperatorCommand())
|
||||
);
|
||||
final List<Permission> childNodes = command.getAdditionalPermissions()
|
||||
.entrySet().stream()
|
||||
.map((entry) -> BukkitCommand.addPermission(
|
||||
plugin,
|
||||
entry.getKey(),
|
||||
"",
|
||||
BukkitCommand.getPermissionDefault(entry.getValue()))
|
||||
)
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (!childNodes.isEmpty()) {
|
||||
BukkitCommand.addPermission(
|
||||
plugin,
|
||||
command.getPermission("*"),
|
||||
command.getUsage(),
|
||||
PermissionDefault.FALSE,
|
||||
childNodes.toArray(new Permission[0])
|
||||
);
|
||||
}
|
||||
|
||||
// Register commodore TAB completion
|
||||
if (CommodoreProvider.isSupported() && plugin.getSettings().doBrigadierTabCompletion()) {
|
||||
BrigadierUtil.registerCommodore(plugin, this, command);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected static Permission addPermission(@NotNull BukkitHuskSync plugin, @NotNull String node,
|
||||
@NotNull String description, @NotNull PermissionDefault permissionDefault,
|
||||
@NotNull Permission... children) {
|
||||
final Map<String, Boolean> childNodes = Arrays.stream(children)
|
||||
.map(Permission::getName)
|
||||
.collect(HashMap::new, (map, child) -> map.put(child, true), HashMap::putAll);
|
||||
|
||||
final PluginManager manager = plugin.getServer().getPluginManager();
|
||||
if (manager.getPermission(node) != null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Permission permission;
|
||||
if (description.isEmpty()) {
|
||||
permission = new Permission(node, permissionDefault, childNodes);
|
||||
} else {
|
||||
permission = new Permission(node, description, permissionDefault, childNodes);
|
||||
}
|
||||
manager.addPermission(permission);
|
||||
|
||||
return permission;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
protected static PermissionDefault getPermissionDefault(boolean isOperatorCommand) {
|
||||
return isOperatorCommand ? PermissionDefault.OP : PermissionDefault.TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commands available on the Bukkit HuskSync implementation
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
HUSKSYNC_COMMAND(HuskSyncCommand::new),
|
||||
USERDATA_COMMAND(UserDataCommand::new),
|
||||
INVENTORY_COMMAND(InventoryCommand::new),
|
||||
ENDER_CHEST_COMMAND(EnderChestCommand::new);
|
||||
|
||||
public final Function<BukkitHuskSync, Command> commandSupplier;
|
||||
|
||||
Type(@NotNull Function<BukkitHuskSync, Command> supplier) {
|
||||
this.commandSupplier = supplier;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Command createCommand(@NotNull BukkitHuskSync plugin) {
|
||||
return commandSupplier.apply(plugin);
|
||||
}
|
||||
|
||||
public static void registerCommands(@NotNull BukkitHuskSync plugin) {
|
||||
Arrays.stream(values())
|
||||
.map((type) -> type.createCommand(plugin))
|
||||
.forEach((command) -> new BukkitCommand(command, plugin).register());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Commands available on the Bukkit HuskSync implementation
|
||||
*/
|
||||
public enum BukkitCommandType {
|
||||
|
||||
HUSKSYNC_COMMAND(new HuskSyncCommand(BukkitHuskSync.getInstance())),
|
||||
USERDATA_COMMAND(new UserDataCommand(BukkitHuskSync.getInstance())),
|
||||
INVENTORY_COMMAND(new InventoryCommand(BukkitHuskSync.getInstance())),
|
||||
ENDER_CHEST_COMMAND(new EnderChestCommand(BukkitHuskSync.getInstance()));
|
||||
|
||||
public final CommandBase commandBase;
|
||||
|
||||
BukkitCommandType(@NotNull CommandBase commandBase) {
|
||||
this.commandBase = commandBase;
|
||||
}
|
||||
}
|
||||
1100
bukkit/src/main/java/net/william278/husksync/data/BukkitData.java
Normal file
1100
bukkit/src/main/java/net/william278/husksync/data/BukkitData.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A mapped player inventory, providing methods to easily access a player's inventory.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitInventoryMap {
|
||||
|
||||
private ItemStack[] contents;
|
||||
|
||||
/**
|
||||
* Creates a new mapped inventory from the given contents.
|
||||
*
|
||||
* @param contents the contents of the inventory
|
||||
*/
|
||||
protected BukkitInventoryMap(ItemStack[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of the inventory.
|
||||
*
|
||||
* @return the contents of the inventory
|
||||
*/
|
||||
public ItemStack[] getContents() {
|
||||
return contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the contents of the inventory.
|
||||
*
|
||||
* @param contents the contents of the inventory
|
||||
*/
|
||||
public void setContents(ItemStack[] contents) {
|
||||
this.contents = contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of the inventory.
|
||||
*
|
||||
* @return the size of the inventory
|
||||
*/
|
||||
public int getSize() {
|
||||
return contents.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the item at the given index.
|
||||
*
|
||||
* @param index the index of the item to get
|
||||
* @return the item at the given index
|
||||
*/
|
||||
public Optional<ItemStack> getItemAt(int index) {
|
||||
if (contents.length >= index) {
|
||||
if (contents[index] == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(contents[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the item at the given index.
|
||||
*
|
||||
* @param itemStack the item to set at the given index
|
||||
* @param index the index of the item to set
|
||||
* @throws IllegalArgumentException if the index is out of bounds
|
||||
*/
|
||||
public void setItemAt(@NotNull ItemStack itemStack, int index) throws IllegalArgumentException {
|
||||
contents[index] = itemStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main inventory contents.
|
||||
*
|
||||
* @return the main inventory contents
|
||||
*/
|
||||
public ItemStack[] getInventory() {
|
||||
final ItemStack[] inventory = new ItemStack[36];
|
||||
System.arraycopy(contents, 0, inventory, 0, Math.min(contents.length, inventory.length));
|
||||
return inventory;
|
||||
}
|
||||
|
||||
public ItemStack[] getHotbar() {
|
||||
final ItemStack[] armor = new ItemStack[9];
|
||||
for (int i = 0; i <= 9; i++) {
|
||||
armor[i] = getItemAt(i).orElse(null);
|
||||
}
|
||||
return armor;
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getOffHand() {
|
||||
return getItemAt(40);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getHelmet() {
|
||||
return getItemAt(39);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getChestplate() {
|
||||
return getItemAt(38);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getLeggings() {
|
||||
return getItemAt(37);
|
||||
}
|
||||
|
||||
public Optional<ItemStack> getBoots() {
|
||||
return getItemAt(36);
|
||||
}
|
||||
|
||||
public ItemStack[] getArmor() {
|
||||
final ItemStack[] armor = new ItemStack[4];
|
||||
for (int i = 36; i < 40; i++) {
|
||||
armor[i - 36] = getItemAt(i).orElse(null);
|
||||
}
|
||||
return armor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,218 +1,265 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.NBTContainer;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.api.HuskSyncAPI;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.List;
|
||||
|
||||
import static net.william278.husksync.data.BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
|
||||
public class BukkitSerializer {
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventoryContents The contents of the inventory
|
||||
* @return The serialized inventory contents
|
||||
*/
|
||||
public static CompletableFuture<String> serializeItemStackArray(@NotNull ItemStack[] inventoryContents)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return an empty string if there is no inventory item data to serialize
|
||||
if (inventoryContents.length == 0) {
|
||||
return "";
|
||||
}
|
||||
protected final HuskSync plugin;
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the inventory array to serialize
|
||||
bukkitOutputStream.writeInt(inventoryContents.length);
|
||||
|
||||
// Write each serialize each ItemStack to the output stream
|
||||
for (ItemStack inventoryItem : inventoryContents) {
|
||||
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new DataSerializationException("Failed to serialize item stack data", e);
|
||||
}
|
||||
});
|
||||
private BukkitSerializer(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link BukkitInventoryMap} from a serialized array of ItemStacks representing the contents of a player's inventory.
|
||||
*
|
||||
* @param serializedPlayerInventory The serialized {@link ItemStack} inventory array
|
||||
* @return The deserialized ItemStacks, mapped for convenience as a {@link BukkitInventoryMap}
|
||||
* @throws DataSerializationException If the serialized item stack array could not be deserialized
|
||||
*/
|
||||
public static CompletableFuture<BukkitInventoryMap> deserializeInventory(@NotNull String serializedPlayerInventory)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> new BukkitInventoryMap(deserializeItemStackArray(serializedPlayerInventory).join()));
|
||||
@SuppressWarnings("unused")
|
||||
public BukkitSerializer(@NotNull HuskSyncAPI api) {
|
||||
this.plugin = api.getPlugin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized inventory data.
|
||||
*
|
||||
* @param serializeItemStackArray The serialized {@link ItemStack} array
|
||||
* @return The deserialized array of {@link ItemStack}s
|
||||
* @throws DataSerializationException If the serialized item stack array could not be deserialized
|
||||
* @implNote Empty slots will be represented by {@code null}
|
||||
*/
|
||||
public static CompletableFuture<ItemStack[]> deserializeItemStackArray(@NotNull String serializeItemStackArray)
|
||||
throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (serializeItemStackArray.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(serializeItemStackArray))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized inventory contents
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new DataSerializationException("Failed to deserialize item stack data", e);
|
||||
}
|
||||
});
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param item The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Object> serializeItemStack(@Nullable ItemStack item) {
|
||||
return item != null ? item.serialize() : null;
|
||||
public static class Inventory extends BukkitSerializer implements Serializer<BukkitData.Items.Inventory> {
|
||||
private static final String ITEMS_TAG = "items";
|
||||
private static final String HELD_ITEM_SLOT_TAG = "held_item_slot";
|
||||
|
||||
public Inventory(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.Inventory deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
final ReadWriteNBT root = NBT.parseNBT(serialized);
|
||||
final ItemStack[] items = root.getItemStackArray(ITEMS_TAG);
|
||||
final int heldItemSlot = root.getInteger(HELD_ITEM_SLOT_TAG);
|
||||
return BukkitData.Items.Inventory.from(
|
||||
items == null ? new ItemStack[INVENTORY_SLOT_COUNT] : items,
|
||||
heldItemSlot
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Items.Inventory data) throws SerializationException {
|
||||
final ReadWriteNBT root = NBT.createNBTObject();
|
||||
root.setItemStackArray(ITEMS_TAG, data.getContents());
|
||||
root.setInteger(HELD_ITEM_SLOT_TAG, data.getHeldItemSlot());
|
||||
return root.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedItemStack The serialized item stack; a String-Object map
|
||||
* @return The deserialized {@link ItemStack}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
@Nullable
|
||||
private static ItemStack deserializeItemStack(@Nullable Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
public static class EnderChest extends BukkitSerializer implements Serializer<BukkitData.Items.EnderChest> {
|
||||
|
||||
public EnderChest(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Items.EnderChest deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
final ItemStack[] items = NBT.itemStackArrayFromNBT(NBT.parseNBT(serialized));
|
||||
return items == null ? BukkitData.Items.EnderChest.empty() : BukkitData.Items.EnderChest.adapt(items);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Items.EnderChest data) throws SerializationException {
|
||||
return NBT.itemStackArrayToNBT(data.getContents()).toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link PotionEffect}s
|
||||
*
|
||||
* @param potionEffects The potion effect array
|
||||
* @return The serialized potion effects
|
||||
*/
|
||||
public static CompletableFuture<String> serializePotionEffectArray(@NotNull PotionEffect[] potionEffects) throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return an empty string if there are no effects to serialize
|
||||
if (potionEffects.length == 0) {
|
||||
return "";
|
||||
}
|
||||
public static class PotionEffects extends BukkitSerializer implements Serializer<BukkitData.PotionEffects> {
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
private static final TypeToken<List<Data.PotionEffects.Effect>> TYPE = new TypeToken<>() {
|
||||
};
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the potion effect array to serialize
|
||||
bukkitOutputStream.writeInt(potionEffects.length);
|
||||
public PotionEffects(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
// Write each serialize each PotionEffect to the output stream
|
||||
for (PotionEffect potionEffect : potionEffects) {
|
||||
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
|
||||
}
|
||||
@Override
|
||||
public BukkitData.PotionEffects deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.PotionEffects.adapt(
|
||||
plugin.getGson().fromJson(serialized, TYPE.getType())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.PotionEffects element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getActiveEffects());
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new DataSerializationException("Failed to serialize potion effect data", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized potion effect data
|
||||
*
|
||||
* @param potionEffectData The serialized {@link PotionEffect} array
|
||||
* @return The {@link PotionEffect}s
|
||||
*/
|
||||
public static CompletableFuture<PotionEffect[]> deserializePotionEffectArray(@NotNull String potionEffectData) throws DataSerializationException {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return empty array if there is no potion effect data (don't apply any effects to the player)
|
||||
if (potionEffectData.isEmpty()) {
|
||||
return new PotionEffect[0];
|
||||
}
|
||||
public static class Advancements extends BukkitSerializer implements Serializer<BukkitData.Advancements> {
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
|
||||
private static final TypeToken<List<Data.Advancements.Advancement>> TYPE = new TypeToken<>() {
|
||||
};
|
||||
|
||||
// Set the potion effects in the array from deserialized PotionEffect data
|
||||
int potionIndex = 0;
|
||||
for (PotionEffect ignored : potionEffects) {
|
||||
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
|
||||
potionIndex++;
|
||||
}
|
||||
public Advancements(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
// Return the finished, serialized potion effect array
|
||||
return potionEffects;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new DataSerializationException("Failed to deserialize potion effects", e);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public BukkitData.Advancements deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.Advancements.from(
|
||||
plugin.getGson().fromJson(serialized, TYPE.getType())
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Advancements element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getCompleted());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param potionEffect The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Object> serializePotionEffect(@Nullable PotionEffect potionEffect) {
|
||||
return potionEffect != null ? potionEffect.serialize() : null;
|
||||
public static class Location extends BukkitSerializer implements Serializer<BukkitData.Location> {
|
||||
|
||||
public Location(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Location deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, BukkitData.Location.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Location element) throws SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedPotionEffect The serialized potion effect; a String-Object map
|
||||
* @return The deserialized {@link PotionEffect}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
@Nullable
|
||||
private static PotionEffect deserializePotionEffect(@Nullable Object serializedPotionEffect) {
|
||||
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
|
||||
public static class Statistics extends BukkitSerializer implements Serializer<BukkitData.Statistics> {
|
||||
|
||||
public Statistics(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.Statistics deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.Statistics.from(plugin.getGson().fromJson(
|
||||
serialized,
|
||||
BukkitData.Statistics.StatisticsMap.class
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.Statistics element) throws SerializationException {
|
||||
return plugin.getGson().toJson(element.getStatisticsSet());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class PersistentData extends BukkitSerializer implements Serializer<BukkitData.PersistentData> {
|
||||
|
||||
public PersistentData(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BukkitData.PersistentData deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return BukkitData.PersistentData.from(new NBTContainer(serialized));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull BukkitData.PersistentData element) throws SerializationException {
|
||||
return element.getPersistentData().toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Health extends Json<BukkitData.Health> implements Serializer<BukkitData.Health> {
|
||||
|
||||
public Health(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Health.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Hunger extends Json<BukkitData.Hunger> implements Serializer<BukkitData.Hunger> {
|
||||
|
||||
public Hunger(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Hunger.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Experience extends Json<BukkitData.Experience> implements Serializer<BukkitData.Experience> {
|
||||
|
||||
public Experience(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.Experience.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class GameMode extends Json<BukkitData.GameMode> implements Serializer<BukkitData.GameMode> {
|
||||
|
||||
public GameMode(@NotNull HuskSync plugin) {
|
||||
super(plugin, BukkitData.GameMode.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static abstract class Json<T extends Data & Adaptable> extends BukkitSerializer implements Serializer<T> {
|
||||
|
||||
private final Class<T> type;
|
||||
|
||||
protected Json(@NotNull HuskSync plugin, Class<T> type) {
|
||||
super(plugin);
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T deserialize(@NotNull String serialized) throws DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String serialize(@NotNull T element) throws SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.util.BukkitMapPersister;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.PlayerInventory;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface BukkitUserDataHolder extends UserDataHolder {
|
||||
|
||||
@Override
|
||||
default Optional<? extends Data> getData(@NotNull Identifier id) {
|
||||
if (!id.isCustom()) {
|
||||
return switch (id.getKeyValue()) {
|
||||
case "inventory" -> getInventory();
|
||||
case "ender_chest" -> getEnderChest();
|
||||
case "potion_effects" -> getPotionEffects();
|
||||
case "advancements" -> getAdvancements();
|
||||
case "location" -> getLocation();
|
||||
case "statistics" -> getStatistics();
|
||||
case "health" -> getHealth();
|
||||
case "hunger" -> getHunger();
|
||||
case "experience" -> getExperience();
|
||||
case "game_mode" -> getGameMode();
|
||||
case "persistent_data" -> getPersistentData();
|
||||
default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id));
|
||||
};
|
||||
}
|
||||
return Optional.ofNullable(getCustomDataStore().get(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setData(@NotNull Identifier id, @NotNull Data data) {
|
||||
if (id.isCustom()) {
|
||||
getCustomDataStore().put(id, data);
|
||||
}
|
||||
UserDataHolder.super.setData(id, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
if ((isDead() && !getPlugin().getSettings().doSynchronizeDeadPlayersChangingServer())) {
|
||||
return Optional.of(BukkitData.Items.Inventory.empty());
|
||||
}
|
||||
final PlayerInventory inventory = getBukkitPlayer().getInventory();
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
getMapPersister().persistLockedMaps(inventory.getContents(), getBukkitPlayer()),
|
||||
inventory.getHeldItemSlot()
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
getMapPersister().persistLockedMaps(getBukkitPlayer().getEnderChest().getContents(), getBukkitPlayer())
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return Optional.of(BukkitData.PotionEffects.from(getBukkitPlayer().getActivePotionEffects()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return Optional.of(BukkitData.Advancements.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.of(BukkitData.Location.adapt(getBukkitPlayer().getLocation()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.of(BukkitData.Statistics.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.of(BukkitData.Health.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.of(BukkitData.Experience.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.of(BukkitData.GameMode.adapt(getBukkitPlayer()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.of(BukkitData.PersistentData.adapt(getBukkitPlayer().getPersistentDataContainer()));
|
||||
}
|
||||
|
||||
boolean isDead();
|
||||
|
||||
@NotNull
|
||||
Player getBukkitPlayer();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
default BukkitMapPersister getMapPersister() {
|
||||
return (BukkitHuskSync) getPlugin();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,24 +1,43 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private boolean cancelled = false;
|
||||
private UserData userData;
|
||||
private final HuskSync plugin;
|
||||
private final DataSnapshot.Packed snapshot;
|
||||
private final User user;
|
||||
private final DataSaveCause saveCause;
|
||||
private boolean cancelled = false;
|
||||
|
||||
protected BukkitDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
protected BukkitDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed snapshot, @NotNull HuskSync plugin) {
|
||||
this.user = user;
|
||||
this.userData = userData;
|
||||
this.saveCause = saveCause;
|
||||
this.snapshot = snapshot;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -38,18 +57,15 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData getUserData() {
|
||||
return userData;
|
||||
@NotNull
|
||||
public DataSnapshot.Packed getData() {
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public void setUserData(@NotNull UserData userData) {
|
||||
this.userData = userData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull DataSaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -57,4 +73,8 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,44 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitEvent() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CompletableFuture<net.william278.husksync.event.Event> fire() {
|
||||
final CompletableFuture<net.william278.husksync.event.Event> eventFireFuture = new CompletableFuture<>();
|
||||
// Don't fire events while the server is shutting down
|
||||
if (!BukkitHuskSync.getInstance().isEnabled()) {
|
||||
eventFireFuture.complete(this);
|
||||
} else {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
Bukkit.getServer().getPluginManager().callEvent(this);
|
||||
eventFireFuture.complete(this);
|
||||
});
|
||||
}
|
||||
return eventFireFuture;
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BukkitEventCannon extends EventCannon {
|
||||
|
||||
public BukkitEventCannon() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> firePreSyncEvent(@NotNull OnlineUser user, @NotNull UserData userData) {
|
||||
return new BukkitPreSyncEvent(((BukkitPlayer) user).getPlayer(), userData).fire();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Event> fireDataSaveEvent(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
return new BukkitDataSaveEvent(user, userData, saveCause).fire();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireSyncCompleteEvent(@NotNull OnlineUser user) {
|
||||
new BukkitSyncCompleteEvent(((BukkitPlayer) user).getPlayer()).fire();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitEventDispatcher extends EventDispatcher {
|
||||
|
||||
@Override
|
||||
default <T extends Event> boolean fireIsCancelled(@NotNull T event) {
|
||||
Bukkit.getPluginManager().callEvent((org.bukkit.event.Event) event);
|
||||
return event instanceof Cancellable cancellable && cancellable.isCancelled();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default PreSyncEvent getPreSyncEvent(@NotNull OnlineUser user, @NotNull DataSnapshot.Packed data) {
|
||||
return new BukkitPreSyncEvent(user, data, getPlugin());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default DataSaveEvent getDataSaveEvent(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
return new BukkitDataSaveEvent(user, data, getPlugin());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default SyncCompleteEvent getSyncCompleteEvent(@NotNull OnlineUser user) {
|
||||
return new BukkitSyncCompleteEvent(user, getPlugin());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +1,54 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
|
||||
|
||||
protected final Player player;
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitPlayerEvent(@NotNull Player player) {
|
||||
protected final OnlineUser player;
|
||||
|
||||
protected BukkitPlayerEvent(@NotNull OnlineUser player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public OnlineUser getUser() {
|
||||
return BukkitPlayer.adapt(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public CompletableFuture<Event> fire() {
|
||||
final CompletableFuture<Event> eventFireFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
Bukkit.getServer().getPluginManager().callEvent(this);
|
||||
eventFireFuture.complete(this);
|
||||
});
|
||||
return eventFireFuture;
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,19 +1,42 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import net.william278.husksync.data.UserData;
|
||||
import org.bukkit.entity.Player;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private final HuskSync plugin;
|
||||
private final DataSnapshot.Packed data;
|
||||
private boolean cancelled = false;
|
||||
private UserData userData;
|
||||
|
||||
protected BukkitPreSyncEvent(@NotNull Player player, @NotNull UserData userData) {
|
||||
protected BukkitPreSyncEvent(@NotNull OnlineUser player, @NotNull DataSnapshot.Packed data, @NotNull HuskSync plugin) {
|
||||
super(player);
|
||||
this.userData = userData;
|
||||
this.data = data;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -27,13 +50,15 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData getUserData() {
|
||||
return userData;
|
||||
@NotNull
|
||||
public DataSnapshot.Packed getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public void setUserData(@NotNull UserData userData) {
|
||||
this.userData = userData;
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@@ -41,4 +66,8 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,34 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.event;
|
||||
|
||||
import org.bukkit.entity.Player;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitSyncCompleteEvent(@NotNull Player player) {
|
||||
protected BukkitSyncCompleteEvent(@NotNull OnlineUser player, @NotNull HuskSync plugin) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
@@ -16,4 +37,8 @@ public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCo
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitDeathEventListener extends Listener {
|
||||
|
||||
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
default void onPlayerDeathHighest(@NotNull PlayerDeathEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.HIGHEST)) {
|
||||
handlePlayerDeath(event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
default void onPlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.NORMAL)) {
|
||||
handlePlayerDeath(event);
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
default void onPlayerDeathLowest(@NotNull PlayerDeathEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.DEATH_LISTENER, EventListener.Priority.LOWEST)) {
|
||||
handlePlayerDeath(event);
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlayerDeath(@NotNull PlayerDeathEvent player);
|
||||
|
||||
}
|
||||
@@ -1,131 +1,202 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitSerializer;
|
||||
import net.william278.husksync.data.DataSerializationException;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.editor.ItemEditorMenuType;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.entity.ProjectileLaunchEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.inventory.PrepareItemCraftEvent;
|
||||
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEntityEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements Listener {
|
||||
public class BukkitEventListener extends EventListener implements BukkitJoinEventListener, BukkitQuitEventListener,
|
||||
BukkitDeathEventListener, Listener {
|
||||
protected final List<String> blacklistedCommands;
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
|
||||
super(huskSync);
|
||||
this.blacklistedCommands = huskSync.getSettings().getBlacklistedCommandsWhileLocked();
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
|
||||
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
|
||||
@Override
|
||||
public boolean handleEvent(@NotNull ListenerType type, @NotNull Priority priority) {
|
||||
return plugin.getSettings().getEventPriority(type).equals(priority);
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
|
||||
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
|
||||
@Override
|
||||
public void handlePlayerQuit(@NotNull BukkitUser bukkitUser) {
|
||||
final Player player = bukkitUser.getPlayer();
|
||||
if (!bukkitUser.isLocked() && !player.getItemOnCursor().getType().isAir()) {
|
||||
player.getWorld().dropItem(player.getLocation(), player.getItemOnCursor());
|
||||
player.setItemOnCursor(null);
|
||||
}
|
||||
super.handlePlayerQuit(bukkitUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerJoin(@NotNull BukkitUser bukkitUser) {
|
||||
super.handlePlayerJoin(bukkitUser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePlayerDeath(@NotNull PlayerDeathEvent event) {
|
||||
final OnlineUser user = BukkitUser.adapt(event.getEntity(), plugin);
|
||||
|
||||
// If the player is locked or the plugin disabling, clear their drops
|
||||
if (cancelPlayerEvent(user.getUuid())) {
|
||||
event.getDrops().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots on death
|
||||
if (!plugin.getSettings().doSaveOnDeath()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Truncate the dropped items list to the inventory size and save the player's inventory
|
||||
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
|
||||
if (event.getDrops().size() > maxInventorySize) {
|
||||
event.getDrops().subList(maxInventorySize, event.getDrops().size()).clear();
|
||||
}
|
||||
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(event.getDrops()));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onWorldSave(@NotNull WorldSaveEvent event) {
|
||||
CompletableFuture.runAsync(() -> super.handleAsyncWorldSave(event.getWorld().getPlayers().stream()
|
||||
.map(BukkitPlayer::adapt).collect(Collectors.toList())));
|
||||
if (!plugin.getSettings().doSaveOnWorldSave()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots when the world saves
|
||||
plugin.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
|
||||
.stream().map(player -> BukkitUser.adapt(player, plugin))
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onInventoryClose(@NotNull InventoryCloseEvent event) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
final OnlineUser user = BukkitPlayer.adapt(player);
|
||||
plugin.getDataEditor().getEditingInventoryData(user).ifPresent(menu -> {
|
||||
try {
|
||||
BukkitSerializer.serializeItemStackArray(Arrays.copyOf(event.getInventory().getContents(),
|
||||
menu.itemEditorMenuType == ItemEditorMenuType.INVENTORY_VIEWER
|
||||
? player.getInventory().getSize()
|
||||
: player.getEnderChest().getSize())).thenAccept(
|
||||
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
|
||||
} catch (DataSerializationException e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"Failed to serialize inventory data during menu close", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onProjectileLaunch(@NotNull ProjectileLaunchEvent event) {
|
||||
final Projectile projectile = event.getEntity();
|
||||
if (projectile.getShooter() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onDropItem(@NotNull PlayerDropItemEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPickupItem(@NotNull EntityPickupItemEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteract(@NotNull PlayerInteractEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerInteractEntity(@NotNull PlayerInteractEntityEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockPlace(@NotNull BlockPlaceEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onBlockBreak(@NotNull BlockBreakEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
if (event.getWhoClicked() instanceof Player player) {
|
||||
event.setCancelled(cancelInventoryClick(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) {
|
||||
event.getDrops().clear();
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getWhoClicked().getUniqueId()));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onCraftItem(@NotNull PrepareItemCraftEvent event) {
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(player.getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
|
||||
public void onPermissionCommand(@NotNull PlayerCommandPreprocessEvent event) {
|
||||
final String[] commandArgs = event.getMessage().substring(1).split(" ");
|
||||
final String commandLabel = commandArgs[0].toLowerCase(Locale.ENGLISH);
|
||||
|
||||
if (blacklistedCommands.contains("*") || blacklistedCommands.contains(commandLabel)) {
|
||||
event.setCancelled(cancelPlayerEvent(event.getPlayer().getUniqueId()));
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitJoinEventListener extends Listener {
|
||||
|
||||
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
default void onPlayerJoinHighest(@NotNull PlayerJoinEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.HIGHEST)) {
|
||||
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
default void onPlayerJoin(@NotNull PlayerJoinEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.NORMAL)) {
|
||||
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
default void onPlayerJoinLowest(@NotNull PlayerJoinEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.JOIN_LISTENER, EventListener.Priority.LOWEST)) {
|
||||
handlePlayerJoin(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlayerJoin(@NotNull BukkitUser player);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.BukkitUser;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface BukkitQuitEventListener extends Listener {
|
||||
|
||||
boolean handleEvent(@NotNull EventListener.ListenerType type, @NotNull EventListener.Priority priority);
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
default void onPlayerQuitHighest(@NotNull PlayerQuitEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.HIGHEST)) {
|
||||
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
|
||||
default void onPlayerQuit(@NotNull PlayerQuitEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.NORMAL)) {
|
||||
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
|
||||
default void onPlayerQuitLowest(@NotNull PlayerQuitEvent event) {
|
||||
if (handleEvent(EventListener.ListenerType.QUIT_LISTENER, EventListener.Priority.LOWEST)) {
|
||||
handlePlayerQuit(BukkitUser.adapt(event.getPlayer(), getPlugin()));
|
||||
}
|
||||
}
|
||||
|
||||
void handlePlayerQuit(@NotNull BukkitUser player);
|
||||
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -1,23 +1,44 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import me.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.hslmigrator.HSLConverter;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.BukkitLegacyConverter;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -32,50 +53,48 @@ public class LegacyMigrator extends Migrator {
|
||||
private String sourcePlayersTable;
|
||||
private String sourceDataTable;
|
||||
|
||||
private final String minecraftVersion;
|
||||
|
||||
public LegacyMigrator(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.hslConverter = HSLConverter.getInstance();
|
||||
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
this.sourceHost = plugin.getSettings().getMySqlHost();
|
||||
this.sourcePort = plugin.getSettings().getMySqlPort();
|
||||
this.sourceUsername = plugin.getSettings().getMySqlUsername();
|
||||
this.sourcePassword = plugin.getSettings().getMySqlPassword();
|
||||
this.sourceDatabase = plugin.getSettings().getMySqlDatabase();
|
||||
this.sourcePlayersTable = "husksync_players";
|
||||
this.sourceDataTable = "husksync_data";
|
||||
this.minecraftVersion = plugin.getMinecraftVersion().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> start() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
|
||||
plugin.log(Level.INFO, "Starting migration of legacy HuskSync v1.x data...");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
return plugin.supplyAsync(() -> {
|
||||
// Wipe the existing database, preparing it for data import
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase().join();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase();
|
||||
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
|
||||
|
||||
// Create a new data source for the mpdb converter
|
||||
try (final HikariDataSource connectionPool = new HikariDataSource()) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to legacy database...");
|
||||
plugin.log(Level.INFO, "Establishing connection to legacy database...");
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
connectionPool.setUsername(sourceUsername);
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
|
||||
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the legacy database...");
|
||||
plugin.log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
|
||||
final List<LegacyData> dataToMigrate = new ArrayList<>();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
|
||||
FROM `%source_players_table%`
|
||||
INNER JOIN `%source_data_table%`
|
||||
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`;
|
||||
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`
|
||||
WHERE `username` IS NOT NULL;
|
||||
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
|
||||
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
|
||||
try (final ResultSet resultSet = statement.executeQuery()) {
|
||||
@@ -104,26 +123,36 @@ public class LegacyMigrator extends Migrator {
|
||||
resultSet.getString("location")
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 25 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
|
||||
if (playersMigrated % 50 == 0) {
|
||||
plugin.log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the latest HuskSync user data format...");
|
||||
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData ->
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
}))));
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
|
||||
plugin.log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> {
|
||||
final DataSnapshot.Packed convertedData = data.toUserData(hslConverter, plugin);
|
||||
plugin.getDatabase().ensureUser(data.user());
|
||||
try {
|
||||
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().getUsername() + ": " + e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
|
||||
}
|
||||
});
|
||||
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Error while migrating legacy data: " + e.getMessage() + " - are your source database credentials correct?", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -132,7 +161,7 @@ public class LegacyMigrator extends Migrator {
|
||||
@Override
|
||||
public void handleConfigurationCommand(@NotNull String[] args) {
|
||||
if (args.length == 2) {
|
||||
if (switch (args[0].toLowerCase()) {
|
||||
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "host" -> {
|
||||
this.sourceHost = args[1];
|
||||
yield true;
|
||||
@@ -167,15 +196,15 @@ public class LegacyMigrator extends Migrator {
|
||||
}
|
||||
default -> false;
|
||||
}) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,11 +228,11 @@ public class LegacyMigrator extends Migrator {
|
||||
This will migrate all user data from HuskSync v1.x to
|
||||
HuskSync v2.x's new format. To perform the migration,
|
||||
please follow the steps below carefully.
|
||||
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
|
||||
|
||||
STEP 1] Please ensure no players are on any servers.
|
||||
|
||||
|
||||
STEP 2] HuskSync will need to connect to the database
|
||||
used to hold the existing, legacy HuskSync data.
|
||||
If this is the same database as the one you are
|
||||
@@ -223,12 +252,12 @@ public class LegacyMigrator extends Migrator {
|
||||
using the command:
|
||||
"husksync migrate legacy set <parameter> <value>"
|
||||
(e.g.: "husksync migrate legacy set host 1.2.3.4")
|
||||
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
server. Please make sure you're happy with this
|
||||
before proceeding.
|
||||
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate legacy start"
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
@@ -249,51 +278,69 @@ public class LegacyMigrator extends Migrator {
|
||||
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
|
||||
|
||||
@NotNull
|
||||
public CompletableFuture<UserData> toUserData(@NotNull HSLConverter converter,
|
||||
@NotNull String minecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
final DataSerializer.StatisticData legacyStatisticData = converter
|
||||
.deserializeStatisticData(serializedStatistics);
|
||||
final StatisticsData convertedStatisticData = new StatisticsData(
|
||||
convertStatisticMap(legacyStatisticData.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(legacyStatisticData.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(legacyStatisticData.itemStatisticValues()),
|
||||
convertEntityStatisticMap(legacyStatisticData.entityStatisticValues()));
|
||||
public DataSnapshot.Packed toUserData(@NotNull HSLConverter converter, @NotNull HuskSync plugin) {
|
||||
try {
|
||||
final DataSerializer.StatisticData stats = converter.deserializeStatisticData(serializedStatistics);
|
||||
final DataSerializer.PlayerLocation loc = converter.deserializePlayerLocationData(serializedLocation);
|
||||
final BukkitLegacyConverter adapter = (BukkitLegacyConverter) plugin.getLegacyConverter()
|
||||
.orElseThrow(() -> new IllegalStateException("Legacy converter not present"));
|
||||
|
||||
final List<AdvancementData> convertedAdvancements = converter
|
||||
.deserializeAdvancementData(serializedAdvancements)
|
||||
.stream().map(data -> new AdvancementData(data.key(), data.criteriaMap())).toList();
|
||||
return DataSnapshot.builder(plugin)
|
||||
// Inventory
|
||||
.inventory(BukkitData.Items.Inventory.from(
|
||||
adapter.deserializeLegacyItemStacks(serializedInventory),
|
||||
selectedSlot
|
||||
))
|
||||
|
||||
final DataSerializer.PlayerLocation legacyLocationData = converter
|
||||
.deserializePlayerLocationData(serializedLocation);
|
||||
final LocationData convertedLocationData = new LocationData(
|
||||
legacyLocationData == null ? "world" : legacyLocationData.worldName(),
|
||||
UUID.randomUUID(),
|
||||
"NORMAL",
|
||||
legacyLocationData == null ? 0d : legacyLocationData.x(),
|
||||
legacyLocationData == null ? 64d : legacyLocationData.y(),
|
||||
legacyLocationData == null ? 0d : legacyLocationData.z(),
|
||||
legacyLocationData == null ? 90f : legacyLocationData.yaw(),
|
||||
legacyLocationData == null ? 180f : legacyLocationData.pitch());
|
||||
// Ender chest
|
||||
.enderChest(BukkitData.Items.EnderChest.adapt(
|
||||
adapter.deserializeLegacyItemStacks(serializedEnderChest)
|
||||
))
|
||||
|
||||
return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation,
|
||||
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying),
|
||||
new ItemData(serializedInventory), new ItemData(serializedEnderChest),
|
||||
new PotionEffectData(serializedPotionEffects), convertedAdvancements,
|
||||
convertedStatisticData, convertedLocationData,
|
||||
new PersistentDataContainerData(new HashMap<>()),
|
||||
minecraftVersion);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
// Location
|
||||
.location(BukkitData.Location.from(
|
||||
loc == null ? 0d : loc.x(),
|
||||
loc == null ? 64d : loc.y(),
|
||||
loc == null ? 0d : loc.z(),
|
||||
loc == null ? 90f : loc.yaw(),
|
||||
loc == null ? 180f : loc.pitch(),
|
||||
new Data.Location.World(
|
||||
loc == null ? "world" : loc.worldName(),
|
||||
UUID.randomUUID(), "NORMAL"
|
||||
)))
|
||||
|
||||
// Advancements
|
||||
.advancements(BukkitData.Advancements.from(converter
|
||||
.deserializeAdvancementData(serializedAdvancements).stream()
|
||||
.map(data -> Data.Advancements.Advancement.adapt(data.key(), data.criteriaMap()))
|
||||
.toList()))
|
||||
|
||||
// Stats
|
||||
.statistics(BukkitData.Statistics.from(
|
||||
BukkitData.Statistics.createStatisticsMap(
|
||||
convertStatisticMap(stats.untypedStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.blockStatisticValues()),
|
||||
convertMaterialStatisticMap(stats.itemStatisticValues()),
|
||||
convertEntityStatisticMap(stats.entityStatisticValues())
|
||||
)))
|
||||
|
||||
// Health, hunger, experience & game mode
|
||||
.health(BukkitData.Health.from(health, maxHealth, healthScale))
|
||||
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from(gameMode, isFlying, isFlying))
|
||||
|
||||
// Build & pack into new format
|
||||
.saveCause(DataSnapshot.SaveCause.LEGACY_MIGRATION).buildAndPack();
|
||||
} catch (Throwable e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Integer> convertStatisticMap(@NotNull HashMap<Statistic, Integer> rawMap) {
|
||||
final HashMap<String, Integer> convertedMap = new HashMap<>();
|
||||
for (Map.Entry<Statistic, Integer> entry : rawMap.entrySet()) {
|
||||
convertedMap.put(entry.getKey().toString(), entry.getValue());
|
||||
convertedMap.put(entry.getKey().getKey().toString(), entry.getValue());
|
||||
}
|
||||
return convertedMap;
|
||||
}
|
||||
@@ -302,8 +349,8 @@ public class LegacyMigrator extends Migrator {
|
||||
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
|
||||
for (Map.Entry<Statistic, HashMap<Material, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<Material, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().toString(), materialEntry.getValue());
|
||||
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
|
||||
}
|
||||
}
|
||||
return convertedMap;
|
||||
@@ -313,8 +360,8 @@ public class LegacyMigrator extends Migrator {
|
||||
final Map<String, Map<String, Integer>> convertedMap = new HashMap<>();
|
||||
for (Map.Entry<Statistic, HashMap<EntityType, Integer>> entry : rawMap.entrySet()) {
|
||||
for (Map.Entry<EntityType, Integer> materialEntry : entry.getValue().entrySet()) {
|
||||
convertedMap.computeIfAbsent(entry.getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().toString(), materialEntry.getValue());
|
||||
convertedMap.computeIfAbsent(entry.getKey().getKey().toString(), k -> new HashMap<>())
|
||||
.put(materialEntry.getKey().getKey().toString(), materialEntry.getValue());
|
||||
}
|
||||
}
|
||||
return convertedMap;
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.mpdbconverter.MPDBConverter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -18,11 +37,12 @@ import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link UserData}
|
||||
* A migrator for migrating MySQLPlayerDataBridge data to HuskSync {@link DataSnapshot}s
|
||||
*/
|
||||
public class MpdbMigrator extends Migrator {
|
||||
|
||||
@@ -35,45 +55,46 @@ public class MpdbMigrator extends Migrator {
|
||||
private String sourceInventoryTable;
|
||||
private String sourceEnderChestTable;
|
||||
private String sourceExperienceTable;
|
||||
private final String minecraftVersion;
|
||||
|
||||
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
|
||||
public MpdbMigrator(@NotNull BukkitHuskSync plugin) {
|
||||
super(plugin);
|
||||
this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge);
|
||||
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
this.mpdbConverter = MPDBConverter.getInstance(Objects.requireNonNull(
|
||||
Bukkit.getPluginManager().getPlugin("MySQLPlayerDataBridge"),
|
||||
"MySQLPlayerDataBridge dependency not found!"
|
||||
));
|
||||
this.sourceHost = plugin.getSettings().getMySqlHost();
|
||||
this.sourcePort = plugin.getSettings().getMySqlPort();
|
||||
this.sourceUsername = plugin.getSettings().getMySqlUsername();
|
||||
this.sourcePassword = plugin.getSettings().getMySqlPassword();
|
||||
this.sourceDatabase = plugin.getSettings().getMySqlDatabase();
|
||||
this.sourceInventoryTable = "mpdb_inventory";
|
||||
this.sourceEnderChestTable = "mpdb_enderchest";
|
||||
this.sourceExperienceTable = "mpdb_experience";
|
||||
this.minecraftVersion = plugin.getMinecraftVersion().toString();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> start() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
|
||||
plugin.log(Level.INFO, "Starting migration from MySQLPlayerDataBridge to HuskSync...");
|
||||
final long startTime = System.currentTimeMillis();
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
return plugin.supplyAsync(() -> {
|
||||
// Wipe the existing database, preparing it for data import
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase().join();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
plugin.log(Level.INFO, "Preparing existing database (wiping)...");
|
||||
plugin.getDatabase().wipeDatabase();
|
||||
plugin.log(Level.INFO, "Successfully wiped user data database (took " + (System.currentTimeMillis() - startTime) + "ms)");
|
||||
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + sourceHost + ":" + sourcePort + "/" + sourceDatabase;
|
||||
|
||||
// Create a new data source for the mpdb converter
|
||||
try (final HikariDataSource connectionPool = new HikariDataSource()) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
|
||||
plugin.log(Level.INFO, "Establishing connection to MySQLPlayerDataBridge database...");
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
connectionPool.setUsername(sourceUsername);
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase(Locale.ENGLISH));
|
||||
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database...");
|
||||
plugin.log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
|
||||
final List<MpdbData> dataToMigrate = new ArrayList<>();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
@@ -101,25 +122,29 @@ public class MpdbMigrator extends Migrator {
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 25 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
|
||||
plugin.log(Level.INFO, "Downloaded MySQLPlayerDataBridge data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data...");
|
||||
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData ->
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
})));
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
plugin.log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
|
||||
plugin.log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> {
|
||||
final DataSnapshot.Packed convertedData = data.toUserData(mpdbConverter, plugin);
|
||||
plugin.getDatabase().ensureUser(data.user());
|
||||
plugin.getDatabase().addSnapshot(data.user(), convertedData);
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
|
||||
}
|
||||
});
|
||||
plugin.log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
plugin.log(Level.SEVERE, "Error while migrating data: " + e.getMessage() + " - are your source database credentials correct?");
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -128,7 +153,7 @@ public class MpdbMigrator extends Migrator {
|
||||
@Override
|
||||
public void handleConfigurationCommand(@NotNull String[] args) {
|
||||
if (args.length == 2) {
|
||||
if (switch (args[0].toLowerCase()) {
|
||||
if (switch (args[0].toLowerCase(Locale.ENGLISH)) {
|
||||
case "host" -> {
|
||||
this.sourceHost = args[1];
|
||||
yield true;
|
||||
@@ -167,15 +192,15 @@ public class MpdbMigrator extends Migrator {
|
||||
}
|
||||
default -> false;
|
||||
}) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, "Successfully set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]));
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
plugin.log(Level.INFO, "Invalid operation, could not set " + args[0] + " to " +
|
||||
obfuscateDataString(args[1]) + " (is it a valid option?)");
|
||||
}
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, getHelpMenu());
|
||||
plugin.log(Level.INFO, getHelpMenu());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +223,14 @@ public class MpdbMigrator extends Migrator {
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
This will migrate inventories, ender chests and XP
|
||||
from the MySQLPlayerDataBridge plugin to HuskSync.
|
||||
|
||||
|
||||
To prevent excessive migration times, other non-vital
|
||||
data will not be transferred.
|
||||
|
||||
|
||||
[!] Existing data in the database will be wiped. [!]
|
||||
|
||||
|
||||
STEP 1] Please ensure no players are on any servers.
|
||||
|
||||
|
||||
STEP 2] HuskSync will need to connect to the database
|
||||
used to hold the source MySQLPlayerDataBridge data.
|
||||
Please check these database parameters are OK:
|
||||
@@ -221,12 +246,12 @@ public class MpdbMigrator extends Migrator {
|
||||
using the command:
|
||||
"husksync migrate mpdb set <parameter> <value>"
|
||||
(e.g.: "husksync migrate mpdb set host 1.2.3.4")
|
||||
|
||||
|
||||
STEP 3] HuskSync will migrate data into the database
|
||||
tables configures in the config.yml file of this
|
||||
server. Please make sure you're happy with this
|
||||
before proceeding.
|
||||
|
||||
|
||||
STEP 4] To start the migration, please run:
|
||||
"husksync migrate mpdb start"
|
||||
""".replaceAll(Pattern.quote("%source_host%"), obfuscateDataString(sourceHost))
|
||||
@@ -250,42 +275,43 @@ public class MpdbMigrator extends Migrator {
|
||||
* @param expProgress The player's current XP progress
|
||||
* @param totalExp The player's total XP score
|
||||
*/
|
||||
private record MpdbData(@NotNull User user, @NotNull String serializedInventory,
|
||||
@NotNull String serializedArmor, @NotNull String serializedEnderChest,
|
||||
int expLevel, float expProgress, int totalExp) {
|
||||
private record MpdbData(
|
||||
@NotNull User user,
|
||||
@NotNull String serializedInventory,
|
||||
@NotNull String serializedArmor,
|
||||
@NotNull String serializedEnderChest,
|
||||
int expLevel,
|
||||
float expProgress,
|
||||
int totalExp
|
||||
) {
|
||||
|
||||
/**
|
||||
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link UserData} object format
|
||||
* Converts exported MySQLPlayerDataBridge data into HuskSync's {@link DataSnapshot} object format
|
||||
*
|
||||
* @param converter The {@link MPDBConverter} to use for converting to {@link ItemStack}s
|
||||
* @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object
|
||||
* @return A {@link CompletableFuture} that will resolve to the converted {@link DataSnapshot} object
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<UserData> toUserData(@NotNull MPDBConverter converter,
|
||||
@NotNull String minecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Combine inventory and armour
|
||||
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
|
||||
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
|
||||
for (int i = 36; i < 36 + armor.length; i++) {
|
||||
inventory.setItem(i, armor[i - 36]);
|
||||
}
|
||||
public DataSnapshot.Packed toUserData(@NotNull MPDBConverter converter, @NotNull HuskSync plugin) {
|
||||
// Combine inventory and armor
|
||||
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
inventory.setContents(converter.getItemStackFromSerializedData(serializedInventory));
|
||||
final ItemStack[] armor = converter.getItemStackFromSerializedData(serializedArmor).clone();
|
||||
for (int i = 36; i < 36 + armor.length; i++) {
|
||||
inventory.setItem(i, armor[i - 36]);
|
||||
}
|
||||
final ItemStack[] enderChest = converter.getItemStackFromSerializedData(serializedEnderChest);
|
||||
|
||||
// Create user data record
|
||||
return new UserData(new StatusData(20, 20, 0, 20, 10,
|
||||
1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
|
||||
false),
|
||||
new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()),
|
||||
new ItemData(BukkitSerializer.serializeItemStackArray(converter
|
||||
.getItemStackFromSerializedData(serializedEnderChest)).join()),
|
||||
new PotionEffectData(""), new ArrayList<>(),
|
||||
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()),
|
||||
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
|
||||
0f, 0f),
|
||||
new PersistentDataContainerData(new HashMap<>()),
|
||||
minecraftVersion);
|
||||
});
|
||||
// Create user data record
|
||||
return DataSnapshot.builder(plugin)
|
||||
.inventory(BukkitData.Items.Inventory.from(inventory.getContents(), 0))
|
||||
.enderChest(BukkitData.Items.EnderChest.adapt(enderChest))
|
||||
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
|
||||
.gameMode(BukkitData.GameMode.from("SURVIVAL", false, false))
|
||||
.saveCause(DataSnapshot.SaveCause.MPDB_MIGRATION)
|
||||
.buildAndPack();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,497 +0,0 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.util.Version;
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitPlayer extends OnlineUser {
|
||||
|
||||
private final Player player;
|
||||
|
||||
private BukkitPlayer(@NotNull Player player) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public static BukkitPlayer adapt(@NotNull Player player) {
|
||||
return new BukkitPlayer(player);
|
||||
}
|
||||
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatusData> getStatus() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final double maxHealth = getMaxHealth(player);
|
||||
return new StatusData(Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0d,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().name(),
|
||||
player.getAllowFlight() && player.isFlying());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
|
||||
@NotNull List<StatusDataFlag> statusDataFlags) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.getBaseValue();
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH)) {
|
||||
if (statusData.maxHealth != 0d) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.setBaseValue(statusData.maxHealth);
|
||||
currentMaxHealth = statusData.maxHealth;
|
||||
}
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) {
|
||||
final double currentHealth = player.getHealth();
|
||||
if (statusData.health != currentHealth) {
|
||||
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
|
||||
if (healthToSet <= 0) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet));
|
||||
} else {
|
||||
player.setHealth(healthToSet);
|
||||
}
|
||||
}
|
||||
|
||||
if (statusData.healthScale != 0d) {
|
||||
player.setHealthScale(statusData.healthScale);
|
||||
} else {
|
||||
player.setHealthScale(statusData.maxHealth);
|
||||
}
|
||||
player.setHealthScaled(statusData.healthScale != 0D);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_HUNGER)) {
|
||||
player.setFoodLevel(statusData.hunger);
|
||||
player.setSaturation(statusData.saturation);
|
||||
player.setExhaustion(statusData.saturationExhaustion);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT)) {
|
||||
player.getInventory().setHeldItemSlot(statusData.selectedItemSlot);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE)) {
|
||||
player.setTotalExperience(statusData.totalExperience);
|
||||
player.setLevel(statusData.expLevel);
|
||||
player.setExp(statusData.expProgress);
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE)) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () ->
|
||||
player.setGameMode(GameMode.valueOf(statusData.gameMode)));
|
||||
}
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_FLYING)) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
if (statusData.isFlying) {
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
}
|
||||
player.setFlying(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ItemData> getInventory() {
|
||||
return BukkitSerializer.serializeItemStackArray(player.getInventory().getContents())
|
||||
.thenApply(ItemData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInventory(@NotNull ItemData itemData) {
|
||||
return BukkitSerializer.deserializeInventory(itemData.serializedItems).thenApplyAsync(contents -> {
|
||||
final CompletableFuture<Void> inventorySetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
player.getInventory().setContents(contents.getContents());
|
||||
inventorySetFuture.complete(null);
|
||||
});
|
||||
return inventorySetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ItemData> getEnderChest() {
|
||||
return BukkitSerializer.serializeItemStackArray(player.getEnderChest().getContents())
|
||||
.thenApply(ItemData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setEnderChest(@NotNull ItemData enderChestData) {
|
||||
return BukkitSerializer.deserializeItemStackArray(enderChestData.serializedItems).thenApplyAsync(contents -> {
|
||||
final CompletableFuture<Void> enderChestSetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
player.getEnderChest().setContents(contents);
|
||||
enderChestSetFuture.complete(null);
|
||||
});
|
||||
return enderChestSetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PotionEffectData> getPotionEffects() {
|
||||
return BukkitSerializer.serializePotionEffectArray(player.getActivePotionEffects()
|
||||
.toArray(new PotionEffect[0])).thenApply(PotionEffectData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
|
||||
return BukkitSerializer.deserializePotionEffectArray(potionEffectData.serializedPotionEffects)
|
||||
.thenApplyAsync(effects -> {
|
||||
final CompletableFuture<Void> potionEffectsSetFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
potionEffectsSetFuture.complete(null);
|
||||
});
|
||||
return potionEffectsSetFuture.join();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<AdvancementData>> getAdvancements() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
final ArrayList<AdvancementData> advancementData = new ArrayList<>();
|
||||
|
||||
// Iterate through the server advancement set and add all advancements to the list
|
||||
serverAdvancements.forEachRemaining(advancement -> {
|
||||
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
|
||||
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
|
||||
advancementProgress.getDateAwarded(criteriaKey)));
|
||||
|
||||
// Only save the advancement if criteria has been completed
|
||||
if (!awardedCriteria.isEmpty()) {
|
||||
advancementData.add(new AdvancementData(advancement.getKey().toString(), awardedCriteria));
|
||||
}
|
||||
});
|
||||
return advancementData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData) {
|
||||
return CompletableFuture.runAsync(() -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
|
||||
// Temporarily disable advancement announcing if needed
|
||||
boolean announceAdvancementUpdate = false;
|
||||
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
announceAdvancementUpdate = true;
|
||||
}
|
||||
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
|
||||
|
||||
// Save current experience and level
|
||||
final int experienceLevel = player.getLevel();
|
||||
final float expProgress = player.getExp();
|
||||
|
||||
// Determines whether the experience might have changed warranting an update
|
||||
final AtomicBoolean correctExperience = new AtomicBoolean(false);
|
||||
|
||||
// Run asynchronously as advancement setting is expensive
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
while (serverAdvancements.hasNext()) {
|
||||
// Iterate through all advancements
|
||||
final Advancement advancement = serverAdvancements.next();
|
||||
final AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
|
||||
|
||||
advancementData.stream().filter(record -> record.key.equals(advancement.getKey().toString())).findFirst().ifPresentOrElse(
|
||||
// Award all criteria that the player does not have that they do on the cache
|
||||
record -> {
|
||||
record.completedCriteria.keySet().stream()
|
||||
.filter(criterion -> !playerProgress.getAwardedCriteria().contains(criterion))
|
||||
.forEach(criterion -> {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).awardCriteria(criterion));
|
||||
correctExperience.set(true);
|
||||
});
|
||||
|
||||
// Revoke all criteria that the player does have but should not
|
||||
new ArrayList<>(playerProgress.getAwardedCriteria()).stream().filter(criterion -> !record.completedCriteria.containsKey(criterion))
|
||||
.forEach(criterion -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion)));
|
||||
|
||||
},
|
||||
// Revoke the criteria as the player shouldn't have any
|
||||
() -> new ArrayList<>(playerProgress.getAwardedCriteria()).forEach(criterion ->
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion))));
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperience.get()) {
|
||||
player.setLevel(experienceLevel);
|
||||
player.setExp(expProgress);
|
||||
correctExperience.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatisticsData> getStatistics() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Map<String, Integer> untypedStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> blockStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> itemStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> entityStatisticValues = new HashMap<>();
|
||||
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
final Map<String, Integer> itemValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isItem)
|
||||
.filter(itemMaterial -> (player.getStatistic(statistic, itemMaterial)) != 0)
|
||||
.forEach(itemMaterial -> itemValues.put(itemMaterial.name(),
|
||||
player.getStatistic(statistic, itemMaterial)));
|
||||
if (!itemValues.isEmpty()) {
|
||||
itemStatisticValues.put(statistic.name(), itemValues);
|
||||
}
|
||||
}
|
||||
case BLOCK -> {
|
||||
final Map<String, Integer> blockValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isBlock)
|
||||
.filter(blockMaterial -> (player.getStatistic(statistic, blockMaterial)) != 0)
|
||||
.forEach(blockMaterial -> blockValues.put(blockMaterial.name(),
|
||||
player.getStatistic(statistic, blockMaterial)));
|
||||
if (!blockValues.isEmpty()) {
|
||||
blockStatisticValues.put(statistic.name(), blockValues);
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
final Map<String, Integer> entityValues = new HashMap<>();
|
||||
Arrays.stream(EntityType.values()).filter(EntityType::isAlive)
|
||||
.filter(entityType -> (player.getStatistic(statistic, entityType)) != 0)
|
||||
.forEach(entityType -> entityValues.put(entityType.name(),
|
||||
player.getStatistic(statistic, entityType)));
|
||||
if (!entityValues.isEmpty()) {
|
||||
entityStatisticValues.put(statistic.name(), entityValues);
|
||||
}
|
||||
}
|
||||
case UNTYPED -> {
|
||||
if (player.getStatistic(statistic) != 0) {
|
||||
untypedStatisticValues.put(statistic.name(), player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StatisticsData(untypedStatisticValues, blockStatisticValues,
|
||||
itemStatisticValues, entityStatisticValues);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Set untyped statistics
|
||||
for (String statistic : statisticsData.untypedStatistics.keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistics.get(statistic));
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (String statistic : statisticsData.blockStatistics.keySet()) {
|
||||
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
|
||||
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (String statistic : statisticsData.itemStatistics.keySet()) {
|
||||
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
|
||||
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (String statistic : statisticsData.entityStatistics.keySet()) {
|
||||
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
|
||||
statisticsData.entityStatistics.get(statistic).get(entityType));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<LocationData> getLocation() {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
new LocationData(player.getWorld().getName(), player.getWorld().getUID(), player.getWorld().getEnvironment().name(),
|
||||
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
|
||||
player.getLocation().getYaw(), player.getLocation().getPitch()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
|
||||
final CompletableFuture<Void> teleportFuture = new CompletableFuture<>();
|
||||
AtomicReference<World> bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName));
|
||||
if (bukkitWorld.get() == null) {
|
||||
bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid));
|
||||
}
|
||||
if (bukkitWorld.get() == null) {
|
||||
Bukkit.getWorlds().stream().filter(world -> world.getEnvironment() == World.Environment
|
||||
.valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set);
|
||||
}
|
||||
if (bukkitWorld.get() != null) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
player.teleport(new Location(bukkitWorld.get(),
|
||||
locationData.x, locationData.y, locationData.z,
|
||||
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
teleportFuture.complete(null);
|
||||
});
|
||||
}
|
||||
return teleportFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final PersistentDataContainer container = player.getPersistentDataContainer();
|
||||
if (container.isEmpty()) {
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
}
|
||||
final HashMap<String, Byte[]> persistentDataMap = new HashMap<>();
|
||||
// Set persistent data keys; ignore keys that we cannot synchronise as byte arrays
|
||||
for (final NamespacedKey key : container.getKeys()) {
|
||||
try {
|
||||
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY)));
|
||||
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||
}
|
||||
}
|
||||
return new PersistentDataContainerData(persistentDataMap);
|
||||
}).exceptionally(throwable -> {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING, "Could not read " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
|
||||
player.getPersistentDataContainer().remove(namespacedKey));
|
||||
persistentDataContainerData.persistentDataMap.keySet().forEach(keyString -> {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
if (key != null) {
|
||||
final byte[] data = ArrayUtils.toPrimitive(persistentDataContainerData
|
||||
.persistentDataMap.get(keyString));
|
||||
player.getPersistentDataContainer().set(key, PersistentDataType.BYTE_ARRAY, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
try {
|
||||
return player == null;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMenu(@NotNull ItemEditorMenu menu) {
|
||||
BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> {
|
||||
final Inventory inventory = Bukkit.createInventory(player, menu.itemEditorMenuType.slotCount,
|
||||
BaseComponent.toLegacyText(menu.menuTitle.toComponent()));
|
||||
inventory.setContents(inventoryContents);
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.openInventory(inventory));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(mineDown.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s maximum health, minus any health boost effects
|
||||
*
|
||||
* @param player The {@link Player} to get the maximum health of
|
||||
* @return The {@link Player}'s max health
|
||||
*/
|
||||
private static double getMaxHealth(@NotNull Player player) {
|
||||
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
|
||||
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
|
||||
assert healthBoostEffect != null;
|
||||
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
|
||||
maxHealth -= healthBoostBonus;
|
||||
}
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.user;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
import net.kyori.adventure.audience.Audience;
|
||||
import net.roxeez.advancement.display.FrameType;
|
||||
import net.william278.andjam.Toast;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.BukkitUserDataHolder;
|
||||
import net.william278.husksync.data.Data;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit platform implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitUser extends OnlineUser implements BukkitUserDataHolder {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final Player player;
|
||||
|
||||
private BukkitUser(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static BukkitUser adapt(@NotNull Player player, @NotNull HuskSync plugin) {
|
||||
return new BukkitUser(player, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bukkit {@link Player} instance of this user
|
||||
*
|
||||
* @return the {@link Player} instance
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Player getPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return player == null || !player.isOnline();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Audience getAudience() {
|
||||
return ((BukkitHuskSync) plugin).getAudiences().player(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendToast(@NotNull MineDown title, @NotNull MineDown description,
|
||||
@NotNull String iconMaterial, @NotNull String backgroundType) {
|
||||
try {
|
||||
final Material material = Material.matchMaterial(iconMaterial);
|
||||
Toast.builder((BukkitHuskSync) plugin)
|
||||
.setTitle(title.toComponent())
|
||||
.setDescription(description.toComponent())
|
||||
.setIcon(material != null ? material : Material.BARRIER)
|
||||
.setFrameType(FrameType.valueOf(backgroundType))
|
||||
.build()
|
||||
.show(player);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.WARNING, "Failed to send toast to player " + player.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showGui(@NotNull Data.Items items, @NotNull MineDown title, boolean editable, int size,
|
||||
@NotNull Consumer<Data.Items> onClose) {
|
||||
final ItemStack[] contents = ((BukkitData.Items) items).getContents();
|
||||
final StorageBuilder builder = Gui.storage().rows((int) Math.ceil(size / 9.0d));
|
||||
if (!editable) {
|
||||
builder.disableAllInteractions();
|
||||
}
|
||||
final StorageGui gui = builder.enableOtherActions()
|
||||
.apply(a -> a.getInventory().setContents(contents))
|
||||
.title(title.toComponent()).create();
|
||||
gui.setCloseGuiAction((close) -> onClose.accept(BukkitData.Items.ItemArray.adapt(
|
||||
Arrays.stream(close.getInventory().getContents()).limit(size).toArray(ItemStack[]::new)
|
||||
)));
|
||||
plugin.runSync(() -> gui.open(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() <= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLocked() {
|
||||
return plugin.getLockedPlayers().contains(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNpc() {
|
||||
return player.hasMetadata("NPC");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Player getBukkitPlayer() {
|
||||
return player;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.BukkitData;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitLegacyConverter extends LegacyConverter {
|
||||
|
||||
public BukkitLegacyConverter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id,
|
||||
@NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException {
|
||||
final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
|
||||
final int version = object.getInt("format_version");
|
||||
if (version != 3) {
|
||||
plugin.log(Level.WARNING, String.format("Converting data from older v2 data format (%s).", version));
|
||||
}
|
||||
|
||||
// Read legacy data from the JSON object
|
||||
final DataSnapshot.Builder builder = DataSnapshot.builder(plugin)
|
||||
.id(id).timestamp(timestamp)
|
||||
.saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2)
|
||||
.data(readStatusData(object));
|
||||
readInventory(object).ifPresent(builder::inventory);
|
||||
readEnderChest(object).ifPresent(builder::enderChest);
|
||||
readLocation(object).ifPresent(builder::location);
|
||||
readAdvancements(object).ifPresent(builder::advancements);
|
||||
readStatistics(object).ifPresent(builder::statistics);
|
||||
return builder.buildAndPack();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map<Identifier, Data> readStatusData(@NotNull JSONObject object) {
|
||||
if (!object.has("status_data")) {
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
final JSONObject status = object.getJSONObject("status_data");
|
||||
final HashMap<Identifier, Data> containers = new HashMap<>();
|
||||
if (shouldImport(Identifier.HEALTH)) {
|
||||
containers.put(Identifier.HEALTH, BukkitData.Health.from(
|
||||
status.getDouble("health"),
|
||||
status.getDouble("max_health"),
|
||||
status.getDouble("health_scale")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.HUNGER)) {
|
||||
containers.put(Identifier.HUNGER, BukkitData.Hunger.from(
|
||||
status.getInt("hunger"),
|
||||
status.getFloat("saturation"),
|
||||
status.getFloat("saturation_exhaustion")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.EXPERIENCE)) {
|
||||
containers.put(Identifier.EXPERIENCE, BukkitData.Experience.from(
|
||||
status.getInt("total_experience"),
|
||||
status.getInt("experience_level"),
|
||||
status.getFloat("experience_progress")
|
||||
));
|
||||
}
|
||||
if (shouldImport(Identifier.GAME_MODE)) {
|
||||
containers.put(Identifier.GAME_MODE, BukkitData.GameMode.from(
|
||||
status.getString("game_mode"),
|
||||
status.getBoolean("is_flying"),
|
||||
status.getBoolean("is_flying")
|
||||
));
|
||||
}
|
||||
return containers;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.Inventory> readInventory(@NotNull JSONObject object) {
|
||||
if (!object.has("inventory") || !shouldImport(Identifier.INVENTORY)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject inventoryData = object.getJSONObject("inventory");
|
||||
return Optional.of(BukkitData.Items.Inventory.from(
|
||||
deserializeLegacyItemStacks(inventoryData.getString("serialized_items")), 0
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Items.EnderChest> readEnderChest(@NotNull JSONObject object) {
|
||||
if (!object.has("ender_chest") || !shouldImport(Identifier.ENDER_CHEST)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject inventoryData = object.getJSONObject("ender_chest");
|
||||
return Optional.of(BukkitData.Items.EnderChest.adapt(
|
||||
deserializeLegacyItemStacks(inventoryData.getString("serialized_items"))
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Location> readLocation(@NotNull JSONObject object) {
|
||||
if (!object.has("location") || !shouldImport(Identifier.LOCATION)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject locationData = object.getJSONObject("location");
|
||||
return Optional.of(BukkitData.Location.from(
|
||||
locationData.getDouble("x"),
|
||||
locationData.getDouble("y"),
|
||||
locationData.getDouble("z"),
|
||||
locationData.getFloat("yaw"),
|
||||
locationData.getFloat("pitch"),
|
||||
new Data.Location.World(
|
||||
locationData.getString("world_name"),
|
||||
UUID.fromString(locationData.getString("world_uuid")),
|
||||
locationData.getString("world_environment")
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Advancements> readAdvancements(@NotNull JSONObject object) {
|
||||
if (!object.has("advancements") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONArray advancements = object.getJSONArray("advancements");
|
||||
final List<Data.Advancements.Advancement> converted = new ArrayList<>();
|
||||
advancements.iterator().forEachRemaining(o -> {
|
||||
final JSONObject advancement = (JSONObject) JSONObject.wrap(o);
|
||||
final String key = advancement.getString("key");
|
||||
|
||||
final JSONObject criteria = advancement.getJSONObject("completed_criteria");
|
||||
final Map<String, Date> criteriaMap = new LinkedHashMap<>();
|
||||
criteria.keys().forEachRemaining(criteriaKey -> criteriaMap.put(
|
||||
criteriaKey, parseDate(criteria.getString(criteriaKey)))
|
||||
);
|
||||
converted.add(Data.Advancements.Advancement.adapt(key, criteriaMap));
|
||||
});
|
||||
|
||||
return Optional.of(BukkitData.Advancements.from(converted));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Optional<Data.Statistics> readStatistics(@NotNull JSONObject object) {
|
||||
if (!object.has("statistics") || !shouldImport(Identifier.ADVANCEMENTS)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
final JSONObject stats = object.getJSONObject("statistics");
|
||||
return Optional.of(readStatisticMaps(
|
||||
stats.getJSONObject("untyped_statistics"),
|
||||
stats.getJSONObject("block_statistics"),
|
||||
stats.getJSONObject("item_statistics"),
|
||||
stats.getJSONObject("entity_statistics")
|
||||
));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BukkitData.Statistics readStatisticMaps(@NotNull JSONObject untyped, @NotNull JSONObject blocks,
|
||||
@NotNull JSONObject items, @NotNull JSONObject entities) {
|
||||
final Map<Statistic, Integer> genericStats = new HashMap<>();
|
||||
untyped.keys().forEachRemaining(stat -> genericStats.put(Statistic.valueOf(stat), untyped.getInt(stat)));
|
||||
|
||||
final Map<Statistic, Map<Material, Integer>> blockStats = new HashMap<>();
|
||||
blocks.keys().forEachRemaining(stat -> {
|
||||
final JSONObject blockStat = blocks.getJSONObject(stat);
|
||||
final Map<Material, Integer> blockMap = new HashMap<>();
|
||||
blockStat.keys().forEachRemaining(block -> blockMap.put(Material.valueOf(block), blockStat.getInt(block)));
|
||||
blockStats.put(Statistic.valueOf(stat), blockMap);
|
||||
});
|
||||
|
||||
final Map<Statistic, Map<Material, Integer>> itemStats = new HashMap<>();
|
||||
items.keys().forEachRemaining(stat -> {
|
||||
final JSONObject itemStat = items.getJSONObject(stat);
|
||||
final Map<Material, Integer> itemMap = new HashMap<>();
|
||||
itemStat.keys().forEachRemaining(item -> itemMap.put(Material.valueOf(item), itemStat.getInt(item)));
|
||||
itemStats.put(Statistic.valueOf(stat), itemMap);
|
||||
});
|
||||
|
||||
final Map<Statistic, Map<EntityType, Integer>> entityStats = new HashMap<>();
|
||||
entities.keys().forEachRemaining(stat -> {
|
||||
final JSONObject entityStat = entities.getJSONObject(stat);
|
||||
final Map<EntityType, Integer> entityMap = new HashMap<>();
|
||||
entityStat.keys().forEachRemaining(entity -> entityMap.put(EntityType.valueOf(entity), entityStat.getInt(entity)));
|
||||
entityStats.put(Statistic.valueOf(stat), entityMap);
|
||||
});
|
||||
|
||||
return BukkitData.Statistics.from(genericStats, blockStats, itemStats, entityStats);
|
||||
}
|
||||
|
||||
// Deserialize a legacy item stack array
|
||||
@NotNull
|
||||
public ItemStack[] deserializeLegacyItemStacks(@NotNull String items) {
|
||||
// Return an empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (items.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(items))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
final ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
final ItemStack deserialized = deserializeLegacyItemStack(bukkitInputStream.readObject());
|
||||
inventoryContents[slotIndex] = deserialized;
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the converted contents
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
throw new DataAdapter.AdaptionException("Failed to deserialize legacy item stack data", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize a single legacy item stack
|
||||
@Nullable
|
||||
private static ItemStack deserializeLegacyItemStack(@Nullable Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
|
||||
private boolean shouldImport(@NotNull Identifier type) {
|
||||
return plugin.getSettings().isSyncFeatureEnabled(type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Date parseDate(@NotNull String dateString) {
|
||||
try {
|
||||
return new SimpleDateFormat().parse(dateString);
|
||||
} catch (ParseException e) {
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitLogger extends Logger {
|
||||
|
||||
private final java.util.logging.Logger logger;
|
||||
|
||||
public BukkitLogger(@NotNull java.util.logging.Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) {
|
||||
logger.log(level, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message) {
|
||||
logger.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull MineDown mineDown) {
|
||||
logger.log(level, TextComponent.toLegacyText(mineDown.toComponent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(@NotNull String message) {
|
||||
logger.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(@NotNull String message) {
|
||||
logger.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(@NotNull String message) {
|
||||
logger.config(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import de.tr7zw.changeme.nbtapi.NBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
|
||||
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.mapdataapi.MapBanner;
|
||||
import net.william278.mapdataapi.MapData;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.MapMeta;
|
||||
import org.bukkit.map.*;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public interface BukkitMapPersister {
|
||||
|
||||
// The map used to store HuskSync data in ItemStack NBT
|
||||
String MAP_DATA_KEY = "husksync:persisted_locked_map";
|
||||
// The key used to store the serialized map data in NBT
|
||||
String MAP_PIXEL_DATA_KEY = "canvas_data";
|
||||
// The key used to store the map of World UIDs to MapView IDs in NBT
|
||||
String MAP_VIEW_ID_MAPPINGS_KEY = "id_mappings";
|
||||
|
||||
/**
|
||||
* Persist locked maps in an array of {@link ItemStack}s
|
||||
*
|
||||
* @param items the array of {@link ItemStack}s to persist locked maps in
|
||||
* @param delegateRenderer the player to delegate the rendering of map pixel canvases to
|
||||
* @return the array of {@link ItemStack}s with locked maps persisted to serialized NBT
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] persistLockedMaps(@NotNull ItemStack[] items, @NotNull Player delegateRenderer) {
|
||||
if (!getPlugin().getSettings().doPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, map -> this.persistMapView(map, delegateRenderer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply persisted locked maps to an array of {@link ItemStack}s
|
||||
*
|
||||
* @param items the array of {@link ItemStack}s to apply persisted locked maps to
|
||||
* @return the array of {@link ItemStack}s with persisted locked maps applied
|
||||
*/
|
||||
@NotNull
|
||||
default ItemStack[] setMapViews(@NotNull ItemStack[] items) {
|
||||
if (!getPlugin().getSettings().doPersistLockedMaps()) {
|
||||
return items;
|
||||
}
|
||||
return forEachMap(items, this::applyMapView);
|
||||
}
|
||||
|
||||
// Perform an operation on each map in an array of ItemStacks
|
||||
@NotNull
|
||||
private ItemStack[] forEachMap(@NotNull ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
final ItemStack item = items[i];
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
if (item.getType() == Material.FILLED_MAP && item.hasItemMeta()) {
|
||||
items[i] = function.apply(item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack persistMapView(@NotNull ItemStack map, @NotNull Player delegateRenderer) {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
if (!meta.hasMapView()) {
|
||||
return map;
|
||||
}
|
||||
final MapView view = meta.getMapView();
|
||||
if (view == null || view.getWorld() == null || !view.isLocked() || view.isVirtual()) {
|
||||
return map;
|
||||
}
|
||||
|
||||
NBT.modify(map, nbt -> {
|
||||
// Don't save the map's data twice
|
||||
if (nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Render the map
|
||||
final PersistentMapCanvas canvas = new PersistentMapCanvas(view);
|
||||
for (MapRenderer renderer : view.getRenderers()) {
|
||||
renderer.render(view, canvas, delegateRenderer);
|
||||
getPlugin().debug(String.format("Rendered locked map canvas to view (#%s)", view.getId()));
|
||||
}
|
||||
|
||||
// Persist map data
|
||||
final ReadWriteNBT mapData = nbt.getOrCreateCompound(MAP_DATA_KEY);
|
||||
final String worldUid = view.getWorld().getUID().toString();
|
||||
mapData.setByteArray(MAP_PIXEL_DATA_KEY, canvas.extractMapData().toBytes());
|
||||
nbt.getOrCreateCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
|
||||
getPlugin().debug(String.format("Saved data for locked map (#%s, UID: %s)", view.getId(), worldUid));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private ItemStack applyMapView(@NotNull ItemStack map) {
|
||||
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
|
||||
NBT.get(map, nbt -> {
|
||||
if (!nbt.hasTag(MAP_DATA_KEY)) {
|
||||
return;
|
||||
}
|
||||
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
|
||||
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
|
||||
if (mapData == null || mapIds == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Search for an existing map view
|
||||
Optional<String> world = Optional.empty();
|
||||
for (String worldUid : mapIds.getKeys()) {
|
||||
world = Bukkit.getWorlds().stream()
|
||||
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
|
||||
.findFirst();
|
||||
if (world.isPresent()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (world.isPresent()) {
|
||||
final String uid = world.get();
|
||||
final Optional<MapView> existingView = this.getMapView(mapIds.getInteger(uid));
|
||||
if (existingView.isPresent()) {
|
||||
final MapView view = existingView.get();
|
||||
view.setLocked(true);
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the pixel data and generate a map view otherwise
|
||||
final MapData canvasData;
|
||||
try {
|
||||
getPlugin().debug("Deserializing map data from NBT and generating view...");
|
||||
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
|
||||
"Map pixel data is null"));
|
||||
} catch (Throwable e) {
|
||||
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a renderer to the map with the data
|
||||
final MapView view = generateRenderedMap(canvasData);
|
||||
final String worldUid = getDefaultMapWorld().getUID().toString();
|
||||
meta.setMapView(view);
|
||||
map.setItemMeta(meta);
|
||||
|
||||
// Set the map view ID in NBT
|
||||
NBT.modify(map, editable -> {
|
||||
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
|
||||
"Map view ID mappings compound is null")
|
||||
.setInteger(worldUid, view.getId());
|
||||
});
|
||||
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
// Sets the renderer of a map, and returns the generated MapView
|
||||
@NotNull
|
||||
private MapView generateRenderedMap(@NotNull MapData canvasData) {
|
||||
final MapView view = Bukkit.createMap(getDefaultMapWorld());
|
||||
view.getRenderers().clear();
|
||||
|
||||
// Create a new map view renderer with the map data color at each pixel
|
||||
view.addRenderer(new PersistentMapRenderer(canvasData));
|
||||
view.setLocked(true);
|
||||
view.setScale(MapView.Scale.NORMAL);
|
||||
view.setTrackingPosition(false);
|
||||
view.setUnlimitedTracking(false);
|
||||
|
||||
// Set the view to the map and return it
|
||||
setMapView(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static World getDefaultMapWorld() {
|
||||
final World world = Bukkit.getWorlds().get(0);
|
||||
if (world == null) {
|
||||
throw new IllegalStateException("No worlds are loaded on the server!");
|
||||
}
|
||||
return world;
|
||||
}
|
||||
|
||||
default Optional<MapView> getMapView(int id) {
|
||||
return getMapViews().containsKey(id) ? Optional.of(getMapViews().get(id)) : Optional.empty();
|
||||
}
|
||||
|
||||
default void setMapView(@NotNull MapView view) {
|
||||
getMapViews().put(view.getId(), view);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapRenderer} that can be used to render persistently serialized {@link MapData} to a {@link MapView}
|
||||
*/
|
||||
class PersistentMapRenderer extends MapRenderer {
|
||||
|
||||
private final MapData canvasData;
|
||||
|
||||
private PersistentMapRenderer(@NotNull MapData canvasData) {
|
||||
super(false);
|
||||
this.canvasData = canvasData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NotNull MapView map, @NotNull MapCanvas canvas, @NotNull Player player) {
|
||||
// We set the pixels in this order to avoid the map being rendered upside down
|
||||
for (int i = 0; i < 128; i++) {
|
||||
for (int j = 0; j < 128; j++) {
|
||||
canvas.setPixel(j, i, (byte) canvasData.getColorAt(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// Set the map banners and markers
|
||||
final MapCursorCollection cursors = canvas.getCursors();
|
||||
canvasData.getBanners().forEach(banner -> cursors.addCursor(createBannerCursor(banner)));
|
||||
canvas.setCursors(cursors);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static MapCursor createBannerCursor(@NotNull MapBanner banner) {
|
||||
return new MapCursor(
|
||||
(byte) banner.getPosition().getX(),
|
||||
(byte) banner.getPosition().getZ(),
|
||||
(byte) 0,
|
||||
switch (banner.getColor().toLowerCase(Locale.ENGLISH)) {
|
||||
case "white" -> MapCursor.Type.BANNER_WHITE;
|
||||
case "orange" -> MapCursor.Type.BANNER_ORANGE;
|
||||
case "magenta" -> MapCursor.Type.BANNER_MAGENTA;
|
||||
case "light_blue" -> MapCursor.Type.BANNER_LIGHT_BLUE;
|
||||
case "yellow" -> MapCursor.Type.BANNER_YELLOW;
|
||||
case "lime" -> MapCursor.Type.BANNER_LIME;
|
||||
case "pink" -> MapCursor.Type.BANNER_PINK;
|
||||
case "gray" -> MapCursor.Type.BANNER_GRAY;
|
||||
case "light_gray" -> MapCursor.Type.BANNER_LIGHT_GRAY;
|
||||
case "cyan" -> MapCursor.Type.BANNER_CYAN;
|
||||
case "purple" -> MapCursor.Type.BANNER_PURPLE;
|
||||
case "blue" -> MapCursor.Type.BANNER_BLUE;
|
||||
case "brown" -> MapCursor.Type.BANNER_BROWN;
|
||||
case "green" -> MapCursor.Type.BANNER_GREEN;
|
||||
case "red" -> MapCursor.Type.BANNER_RED;
|
||||
default -> MapCursor.Type.BANNER_BLACK;
|
||||
},
|
||||
true,
|
||||
banner.getText().isEmpty() ? null : banner.getText()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A {@link MapCanvas} implementation used for pre-rendering maps to be converted into {@link MapData}
|
||||
*/
|
||||
class PersistentMapCanvas implements MapCanvas {
|
||||
|
||||
private final MapView mapView;
|
||||
private final int[][] pixels = new int[128][128];
|
||||
private MapCursorCollection cursors;
|
||||
|
||||
private PersistentMapCanvas(@NotNull MapView mapView) {
|
||||
this.mapView = mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public MapCursorCollection getCursors() {
|
||||
return cursors == null ? (cursors = new MapCursorCollection()) : cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCursors(@NotNull MapCursorCollection cursors) {
|
||||
this.cursors = cursors;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPixel(int x, int y, byte color) {
|
||||
pixels[x][y] = color;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getPixel(int x, int y) {
|
||||
return (byte) pixels[x][y];
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getBasePixel(int x, int y) {
|
||||
return getPixel(x, y);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawImage(int x, int y, @NotNull Image image) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawText(int x, int y, @NotNull MapFont font, @NotNull String text) {
|
||||
// Not implemented
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getDimension() {
|
||||
return mapView.getWorld() != null ? switch (mapView.getWorld().getEnvironment()) {
|
||||
case NETHER -> "minecraft:the_nether";
|
||||
case THE_END -> "minecraft:the_end";
|
||||
default -> "minecraft:overworld";
|
||||
} : "minecraft:overworld";
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the map data from the canvas. Must be rendered first
|
||||
*
|
||||
* @return the extracted map data
|
||||
*/
|
||||
@NotNull
|
||||
private MapData extractMapData() {
|
||||
final List<MapBanner> banners = new ArrayList<>();
|
||||
for (int i = 0; i < getCursors().size(); i++) {
|
||||
final MapCursor cursor = getCursors().getCursor(i);
|
||||
final String type = cursor.getType().name().toLowerCase(Locale.ENGLISH);
|
||||
if (type.startsWith("banner_")) {
|
||||
banners.add(new MapBanner(
|
||||
type.replaceAll("banner_", ""),
|
||||
cursor.getCaption() == null ? "" : cursor.getCaption(),
|
||||
cursor.getX(),
|
||||
mapView.getWorld() != null ? mapView.getWorld().getSeaLevel() : 128,
|
||||
cursor.getY()
|
||||
));
|
||||
}
|
||||
}
|
||||
return MapData.fromPixels(pixels, getDimension(), (byte) 2, banners, List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Map<Integer, MapView> getMapViews();
|
||||
|
||||
@ApiStatus.Internal
|
||||
@NotNull
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BukkitResourceReader implements ResourceReader {
|
||||
|
||||
private final BukkitHuskSync plugin;
|
||||
|
||||
public BukkitResourceReader(BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull InputStream getResource(String fileName) {
|
||||
return Objects.requireNonNull(plugin.getResource(fileName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull File getDataFolder() {
|
||||
return plugin.getDataFolder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import space.arim.morepaperlib.scheduling.AsynchronousScheduler;
|
||||
import space.arim.morepaperlib.scheduling.RegionalScheduler;
|
||||
import space.arim.morepaperlib.scheduling.ScheduledTask;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
public interface BukkitTask extends Task {
|
||||
|
||||
class Sync extends Task.Sync implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Sync(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final RegionalScheduler scheduler = ((BukkitHuskSync) getPlugin()).getRegionalScheduler();
|
||||
if (delayTicks > 0) {
|
||||
this.task = scheduler.runDelayed(runnable, delayTicks);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Async extends Task.Async implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Async(@NotNull HuskSync plugin, @NotNull Runnable runnable, long delayTicks) {
|
||||
super(plugin, runnable, delayTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
runnable.run();
|
||||
return;
|
||||
}
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
|
||||
if (delayTicks > 0) {
|
||||
plugin.debug("Running async task with delay of " + delayTicks + " ticks");
|
||||
this.task = scheduler.runDelayed(
|
||||
runnable,
|
||||
Duration.of(delayTicks * 50L, ChronoUnit.MILLIS)
|
||||
);
|
||||
} else {
|
||||
this.task = scheduler.run(runnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Repeating extends Task.Repeating implements BukkitTask {
|
||||
|
||||
private ScheduledTask task;
|
||||
|
||||
protected Repeating(@NotNull HuskSync plugin, @NotNull Runnable runnable, long repeatingTicks) {
|
||||
super(plugin, runnable, repeatingTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel() {
|
||||
if (task != null && !cancelled) {
|
||||
task.cancel();
|
||||
}
|
||||
super.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPluginDisabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
final AsynchronousScheduler scheduler = ((BukkitHuskSync) getPlugin()).getAsyncScheduler();
|
||||
this.task = scheduler.runAtFixedRate(
|
||||
runnable, Duration.ZERO,
|
||||
Duration.of(repeatingTicks * 50L, ChronoUnit.MILLIS)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns if the Bukkit HuskSync plugin is disabled
|
||||
default boolean isPluginDisabled() {
|
||||
return !((BukkitHuskSync) getPlugin()).isEnabled();
|
||||
}
|
||||
|
||||
interface Supplier extends Task.Supplier {
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Sync getSyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Sync(getPlugin(), runnable, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Async getAsyncTask(@NotNull Runnable runnable, long delayTicks) {
|
||||
return new Async(getPlugin(), runnable, delayTicks);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
default Task.Repeating getRepeatingTask(@NotNull Runnable runnable, long repeatingTicks) {
|
||||
return new Repeating(getPlugin(), runnable, repeatingTicks);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void cancelTasks() {
|
||||
((BukkitHuskSync) getPlugin()).getScheduler().cancelGlobalTasks();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
3
bukkit/src/main/resources/commodore/enderchest.commodore
Normal file
3
bukkit/src/main/resources/commodore/enderchest.commodore
Normal file
@@ -0,0 +1,3 @@
|
||||
inventory {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
5
bukkit/src/main/resources/commodore/husksync.commodore
Normal file
5
bukkit/src/main/resources/commodore/husksync.commodore
Normal file
@@ -0,0 +1,5 @@
|
||||
husksync {
|
||||
update;
|
||||
about;
|
||||
reload;
|
||||
}
|
||||
3
bukkit/src/main/resources/commodore/inventory.commodore
Normal file
3
bukkit/src/main/resources/commodore/inventory.commodore
Normal file
@@ -0,0 +1,3 @@
|
||||
enderchest {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
35
bukkit/src/main/resources/commodore/userdata.commodore
Normal file
35
bukkit/src/main/resources/commodore/userdata.commodore
Normal file
@@ -0,0 +1,35 @@
|
||||
userdata {
|
||||
view {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
list {
|
||||
name brigadier:string single_word {
|
||||
page brigadier:integer;
|
||||
}
|
||||
}
|
||||
delete {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
restore {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
pin {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
dump {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word {
|
||||
web;
|
||||
file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,15 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.BukkitHuskSync
|
||||
name: 'HuskSync'
|
||||
version: '${version}'
|
||||
main: 'net.william278.husksync.BukkitHuskSync'
|
||||
api-version: 1.16
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
author: 'William278'
|
||||
description: '${description}'
|
||||
website: 'https://william278.net'
|
||||
softdepend:
|
||||
- MysqlPlayerDataBridge
|
||||
- Plan
|
||||
- 'MysqlPlayerDataBridge'
|
||||
- 'Plan'
|
||||
libraries:
|
||||
- 'mysql:mysql-connector-java:8.0.29'
|
||||
- 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
- 'dev.dejvokep:boosted-yaml:1.2'
|
||||
commands:
|
||||
husksync:
|
||||
usage: '/husksync <update/info/reload/migrate>'
|
||||
description: 'Manage the HuskSync plugin'
|
||||
userdata:
|
||||
usage: '/userdata <view/list/delete/restore/pin> <username> [version_uuid]'
|
||||
description: 'View, manage & restore player userdata'
|
||||
inventory:
|
||||
usage: '/inventory <username> [version_uuid]'
|
||||
description: 'View & edit a player''s inventory'
|
||||
enderchest:
|
||||
usage: '/enderchest <username> [version_uuid]'
|
||||
description: 'View & edit a player''s Ender Chest'
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'com.mysql:mysql-connector-j:${mysql_driver_version}'
|
||||
- 'org.mariadb.jdbc:mariadb-java-client:${mariadb_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
@@ -1,31 +1,33 @@
|
||||
dependencies {
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation('redis.clients:jedis:4.2.3') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
implementation ('com.zaxxer:HikariCP:5.0.1') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
|
||||
compileOnly 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690'
|
||||
|
||||
testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690'
|
||||
testCompileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
plugins {
|
||||
id 'java-library'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
dependencies {
|
||||
api 'commons-io:commons-io:2.13.0'
|
||||
api 'org.apache.commons:commons-text:1.10.0'
|
||||
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
|
||||
api 'net.kyori:adventure-api:4.14.0'
|
||||
api 'org.json:json:20230618'
|
||||
api 'com.google.code.gson:gson:2.10.1'
|
||||
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
|
||||
api 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
api 'net.william278:annotaml:2.0.7'
|
||||
api 'net.william278:DesertWell:2.0.4'
|
||||
api 'net.william278:PagineDown:1.1'
|
||||
api('com.zaxxer:HikariCP:5.0.1') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'org.jetbrains:annotations:24.0.1'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
compileOnly "redis.clients:jedis:$jedis_version"
|
||||
compileOnly "com.mysql:mysql-connector-j:$mysql_driver_version"
|
||||
compileOnly "org.mariadb.jdbc:mariadb-java-client:$mariadb_driver_version"
|
||||
compileOnly "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.5.2272'
|
||||
testImplementation "redis.clients:jedis:$jedis_version"
|
||||
testImplementation "org.xerial.snappy:snappy-java:$snappy_version"
|
||||
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3.1'
|
||||
testCompileOnly 'org.jetbrains:annotations:24.0.1'
|
||||
}
|
||||
@@ -1,28 +1,60 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.fatboyindustrial.gsonjavatime.Converters;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import net.william278.annotaml.Annotaml;
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.editor.DataEditor;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.event.EventDispatcher;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.Version;
|
||||
import net.william278.husksync.user.ConsoleUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.util.LegacyConverter;
|
||||
import net.william278.husksync.util.Task;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Abstract implementation of the HuskSync plugin.
|
||||
*/
|
||||
public interface HuskSync {
|
||||
public interface HuskSync extends Task.Supplier, EventDispatcher {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
/**
|
||||
* Returns a set of online players.
|
||||
@@ -58,29 +90,45 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
RedisManager getRedisManager();
|
||||
|
||||
/**
|
||||
* Returns the data adapter implementation
|
||||
*
|
||||
* @return the {@link DataAdapter} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the data editor implementation
|
||||
*
|
||||
* @return the {@link DataEditor} implementation
|
||||
* Returns the data serializer for the given {@link Identifier}
|
||||
*/
|
||||
@NotNull
|
||||
DataEditor getDataEditor();
|
||||
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
|
||||
|
||||
/**
|
||||
* Returns the event firing cannon
|
||||
* Register a data serializer for the given {@link Identifier}
|
||||
*
|
||||
* @return the {@link EventCannon} implementation
|
||||
* @param identifier the {@link Identifier}
|
||||
* @param serializer the {@link Serializer}
|
||||
*/
|
||||
default void registerSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<? extends Data> serializer) {
|
||||
if (identifier.isCustom()) {
|
||||
log(Level.INFO, String.format("Registered custom data type: %s", identifier));
|
||||
}
|
||||
getSerializers().put(identifier, (Serializer<Data>) serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Identifier} for the given key
|
||||
*/
|
||||
default Optional<Identifier> getIdentifier(@NotNull String key) {
|
||||
return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the set of registered data types
|
||||
*
|
||||
* @return the set of registered data types
|
||||
*/
|
||||
@NotNull
|
||||
EventCannon getEventCannon();
|
||||
default Set<Identifier> getRegisteredDataTypes() {
|
||||
return getSerializers().keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of available data {@link Migrator}s
|
||||
@@ -90,6 +138,25 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
List<Migrator> getAvailableMigrators();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user);
|
||||
|
||||
/**
|
||||
* Initialize a faucet of the plugin.
|
||||
*
|
||||
* @param name the name of the faucet
|
||||
* @param runner a runnable for initializing the faucet
|
||||
*/
|
||||
default void initialize(@NotNull String name, @NotNull ThrowingConsumer<HuskSync> runner) {
|
||||
log(Level.INFO, "Initializing " + name + "...");
|
||||
try {
|
||||
runner.accept(this);
|
||||
} catch (Throwable e) {
|
||||
throw new FailedToLoadException("Failed to initialize " + name, e);
|
||||
}
|
||||
log(Level.INFO, "Successfully initialized " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Settings}
|
||||
*
|
||||
@@ -98,6 +165,8 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
void setSettings(@NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Locales}
|
||||
*
|
||||
@@ -106,13 +175,60 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
void setLocales(@NotNull Locales locales);
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Logger}
|
||||
* Returns if a dependency is loaded
|
||||
*
|
||||
* @return the {@link Logger}
|
||||
* @param name the name of the dependency
|
||||
* @return {@code true} if the dependency is loaded, {@code false} otherwise
|
||||
*/
|
||||
boolean isDependencyLoaded(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Get a resource as an {@link InputStream} from the plugin jar
|
||||
*
|
||||
* @param name the path to the resource
|
||||
* @return the {@link InputStream} of the resource
|
||||
*/
|
||||
InputStream getResource(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Returns the plugin data folder
|
||||
*
|
||||
* @return the plugin data folder as a {@link File}
|
||||
*/
|
||||
@NotNull
|
||||
Logger getLoggingAdapter();
|
||||
File getDataFolder();
|
||||
|
||||
/**
|
||||
* Log a message to the console
|
||||
*
|
||||
* @param level the level of the message
|
||||
* @param message the message to log
|
||||
* @param throwable a throwable to log
|
||||
*/
|
||||
void log(@NotNull Level level, @NotNull String message, @NotNull Throwable... throwable);
|
||||
|
||||
/**
|
||||
* Send a debug message to the console, if debug logging is enabled
|
||||
*
|
||||
* @param message the message to log
|
||||
* @param throwable a throwable to log
|
||||
*/
|
||||
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
|
||||
if (getSettings().doDebugLogging()) {
|
||||
log(Level.INFO, String.format("[DEBUG] %s", message), throwable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the console user
|
||||
*
|
||||
* @return the {@link ConsoleUser}
|
||||
*/
|
||||
@NotNull
|
||||
ConsoleUser getConsole();
|
||||
|
||||
/**
|
||||
* Returns the plugin version
|
||||
@@ -131,10 +247,95 @@ public interface HuskSync {
|
||||
Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Reloads the {@link Settings} and {@link Locales} from their respective config files
|
||||
* Returns the platform type
|
||||
*
|
||||
* @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful
|
||||
* @return the platform type
|
||||
*/
|
||||
CompletableFuture<Boolean> reload();
|
||||
@NotNull
|
||||
String getPlatformType();
|
||||
|
||||
/**
|
||||
* Returns the legacy data converter, if it exists
|
||||
*
|
||||
* @return the {@link LegacyConverter}
|
||||
*/
|
||||
Optional<LegacyConverter> getLegacyConverter();
|
||||
|
||||
/**
|
||||
* Reloads the {@link Settings} and {@link Locales} from their respective config files.
|
||||
*/
|
||||
default void loadConfigs() {
|
||||
try {
|
||||
// Load settings
|
||||
setSettings(Annotaml.create(new File(getDataFolder(), "config.yml"), Settings.class).get());
|
||||
|
||||
// Load locales from language preset default
|
||||
final Locales languagePresets = Annotaml.create(
|
||||
Locales.class,
|
||||
Objects.requireNonNull(getResource(String.format("locales/%s.yml", getSettings().getLanguage())))
|
||||
).get();
|
||||
setLocales(Annotaml.create(new File(
|
||||
getDataFolder(),
|
||||
String.format("messages_%s.yml", getSettings().getLanguage())
|
||||
), languagePresets).get());
|
||||
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new FailedToLoadException("Failed to load config or message files", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default UpdateChecker getUpdateChecker() {
|
||||
return UpdateChecker.builder()
|
||||
.currentVersion(getPluginVersion())
|
||||
.endpoint(UpdateChecker.Endpoint.SPIGOT)
|
||||
.resource(Integer.toString(SPIGOT_RESOURCE_ID))
|
||||
.build();
|
||||
}
|
||||
|
||||
default void checkForUpdates() {
|
||||
if (getSettings().doCheckForUpdates()) {
|
||||
getUpdateChecker().check().thenAccept(checked -> {
|
||||
if (!checked.isUpToDate()) {
|
||||
log(Level.WARNING, String.format(
|
||||
"A new version of HuskSync is available: v%s (running v%s)",
|
||||
checked.getLatestVersion(), getPluginVersion())
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
Set<UUID> getLockedPlayers();
|
||||
|
||||
@NotNull
|
||||
Gson getGson();
|
||||
|
||||
@NotNull
|
||||
default Gson createGson() {
|
||||
return Converters.registerOffsetDateTime(new GsonBuilder()).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
final class FailedToLoadException extends IllegalStateException {
|
||||
|
||||
private static final String FORMAT = """
|
||||
HuskSync has failed to load! The plugin will not be enabled and no data will be synchronized.
|
||||
Please make sure the plugin has been setup correctly (https://william278.net/docs/husksync/setup):
|
||||
|
||||
1) Make sure you've entered your MySQL or MariaDB database details correctly in config.yml
|
||||
2) Make sure your Redis server details are also correct in config.yml
|
||||
3) Make sure your config is up-to-date (https://william278.net/docs/husksync/config-files)
|
||||
4) Check the error below for more details
|
||||
|
||||
Caused by: %s""";
|
||||
|
||||
FailedToLoadException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Indicates an exception occurred while initialising the HuskSync plugin
|
||||
*/
|
||||
public class HuskSyncInitializationException extends RuntimeException {
|
||||
public HuskSyncInitializationException(@NotNull String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
|
||||
public interface Adaptable {
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* An adapter that adapts data to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
default <A extends Adaptable> String toString(@NotNull A data) throws AdaptionException {
|
||||
return new String(this.toBytes(data), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a byte array.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The byte array
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a JSON string to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The JSON string to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts an {@link Adaptable} to a JSON string.
|
||||
*
|
||||
* @param data The {@link Adaptable} to adapt
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The JSON string
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
<A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to an {@link Adaptable}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @param type The class type of the {@link Adaptable} to adapt to.
|
||||
* @param <A> The type of the {@link Adaptable}
|
||||
* @return The {@link Adaptable}
|
||||
* @throws AdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
<A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to a string, including decompression if required.
|
||||
*
|
||||
* @param bytes The byte array to convert
|
||||
* @return the string form of the bytes
|
||||
*/
|
||||
@NotNull
|
||||
String bytesToString(byte[] bytes);
|
||||
|
||||
final class AdaptionException extends IllegalStateException {
|
||||
static final String FORMAT = "An exception occurred when adapting serialized/deserialized data: %s";
|
||||
|
||||
public AdaptionException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(String.format(FORMAT, message), cause);
|
||||
}
|
||||
|
||||
public AdaptionException(@NotNull String message) {
|
||||
super(String.format(FORMAT, message));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class GsonAdapter implements DataAdapter {
|
||||
|
||||
private final HuskSync plugin;
|
||||
|
||||
public GsonAdapter(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
return this.toJson(data).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> String toJson(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().toJson(data);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data to JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String bytesToString(byte[] bytes) {
|
||||
return new String(bytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public <A extends Adaptable> A fromJson(@NotNull String data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return plugin.getGson().fromJson(data, type);
|
||||
} catch (Throwable e) {
|
||||
throw new AdaptionException("Failed to adapt data from JSON via Gson", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.adapter;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SnappyGsonAdapter extends GsonAdapter {
|
||||
|
||||
public SnappyGsonAdapter(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to compress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException {
|
||||
try {
|
||||
return super.fromBytes(decompressBytes(data), type);
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@NotNull
|
||||
public String bytesToString(byte[] bytes) {
|
||||
try {
|
||||
return super.bytesToString(decompressBytes(bytes));
|
||||
} catch (IOException e) {
|
||||
throw new AdaptionException("Failed to decompress data through Snappy", e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] decompressBytes(byte[] bytes) throws IOException {
|
||||
return Snappy.uncompress(bytes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BaseHuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
protected BaseHuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's account {@link UUID}, if they exist.
|
||||
*
|
||||
* @param uuid the unique id of the player to get the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's unique id if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.getDatabase().getUser(uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User} by the given player's username (case-insensitive), if they exist.
|
||||
*
|
||||
* @param username the username of the {@link User} instance for
|
||||
* @return future returning the {@link User} instance for the given player's username if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The player does not have to be online, though their username has to be the username
|
||||
* they had when they last joined the server.
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.getDatabase().getUserByName(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link User}'s current {@link UserData}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserData} for
|
||||
* @return future returning the {@link UserData} for the given {@link User} if they exist, otherwise an empty {@link Optional}
|
||||
* @apiNote If the user is not online on the implementing bukkit server,
|
||||
* the {@link UserData} returned will be their last database-saved UserData.
|
||||
* </p>
|
||||
* Because of this, if the user is online on another server on the network,
|
||||
* then the {@link UserData} returned by this method will <i>not necessarily reflective of
|
||||
* their current state</i>
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (user instanceof OnlineUser) {
|
||||
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter()).join();
|
||||
} else {
|
||||
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link UserData} to the database for the given {@link User}.
|
||||
* </p>
|
||||
* If the user is online and on the same cluster, their data will be updated in game.
|
||||
*
|
||||
* @param user the {@link User} to set the {@link UserData} for
|
||||
* @param userData the {@link UserData} to set for the given {@link User}
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
|
||||
return CompletableFuture.runAsync(() ->
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API)
|
||||
.thenRun(() -> plugin.getRedisManager().sendUserDataUpdate(user, userData).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the {@link UserData} of an {@link OnlineUser} to the database
|
||||
*
|
||||
* @param user the {@link OnlineUser} to save the {@link UserData} of
|
||||
* @return future returning void when complete
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the saved {@link UserDataSnapshot} records for the given {@link User}
|
||||
*
|
||||
* @param user the {@link User} to get the {@link UserDataSnapshot} for
|
||||
* @return future returning a list {@link UserDataSnapshot} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote The length of the list of VersionedUserData will correspond to the configured
|
||||
* {@code max_user_data_records} config option
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<List<UserDataSnapshot>> getSavedUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON string representation of the given {@link UserData}
|
||||
*
|
||||
* @param userData the {@link UserData} to get the JSON string representation of
|
||||
* @param prettyPrint whether to pretty print the JSON string
|
||||
* @return the JSON string representation of the given {@link UserData}
|
||||
* @since 2.0
|
||||
*/
|
||||
@NotNull
|
||||
public final String getUserDataJson(@NotNull UserData userData, boolean prettyPrint) {
|
||||
return plugin.getDataAdapter().toJson(userData, prettyPrint);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,458 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.api;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* The base implementation of the HuskSync API, containing cross-platform API calls.
|
||||
* </p>
|
||||
* This class should not be used directly, but rather through platform-specific extending API classes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class HuskSyncAPI {
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Instance of the implementing plugin.
|
||||
*/
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Constructor, instantiating the base API class.
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
protected HuskSyncAPI(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their UUID
|
||||
*
|
||||
* @param uuid The UUID of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUser(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User} by their username
|
||||
*
|
||||
* @param username The username of the user to get
|
||||
* @return A future containing the user, or an empty optional if the user doesn't exist
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull String username) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().getUserByName(username));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new data snapshot of an {@link OnlineUser}'s data.
|
||||
*
|
||||
* @param user The user to create the snapshot of
|
||||
* @return The snapshot of the user's data
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed createSnapshot(@NotNull OnlineUser user) {
|
||||
return snapshotBuilder().saveCause(DataSnapshot.SaveCause.API).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link User}'s current data, as a {@link DataSnapshot.Unpacked}
|
||||
* <p>
|
||||
* If the user is online, this will create a new snapshot of their data with the {@code API} data save cause.
|
||||
* </p>
|
||||
* If the user is offline, this will return the latest snapshot of their data if that exists
|
||||
* (an empty optional will be returned otherwise).
|
||||
*
|
||||
* @param user The user to get the data of
|
||||
* @return A future containing the user's current data, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getCurrentData(@NotNull User user) {
|
||||
return plugin.getRedisManager()
|
||||
.getUserData(UUID.randomUUID(), user)
|
||||
.thenApply(data -> data.or(() -> plugin.getDatabase().getLatestSnapshot(user)))
|
||||
.thenApply(data -> data.map(snapshot -> snapshot.unpack(plugin)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to set the data of
|
||||
* @param data The data to set
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setCurrentData(@NotNull User user, @NotNull DataSnapshot data) {
|
||||
plugin.runAsync(() -> {
|
||||
final DataSnapshot.Packed packed = data instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) data;
|
||||
addSnapshot(user, packed);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, packed);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a user's current data.
|
||||
* <p>
|
||||
* This will update the user's data in the database (creating a new snapshot) and send a data update,
|
||||
* updating the user if they are online.
|
||||
*
|
||||
* @param user The user to edit the data of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editCurrentData(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
getCurrentData(user).thenAccept(optional -> optional.ifPresent(data -> {
|
||||
editor.accept(data);
|
||||
setCurrentData(user, data);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all saved data snapshots for a user
|
||||
*
|
||||
* @param user The user to get the data snapshots of
|
||||
* @return The user's data snapshots
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshots(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getAllSnapshots(user).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific data snapshot for a user
|
||||
*
|
||||
* @param user The user to get the data snapshot of
|
||||
* @param versionId The version ID of the snapshot to get
|
||||
* @return The user's data snapshot, or an empty optional if the user has no data
|
||||
* @see #getSnapshots(User)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<List<DataSnapshot.Unpacked>> getSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getSnapshot(user, versionId).stream()
|
||||
.map(snapshot -> snapshot.unpack(plugin))
|
||||
.toList()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the snapshot of
|
||||
* @param versionId The version ID of the snapshot to edit
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editSnapshot(@NotNull User user, @NotNull UUID versionId,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getSnapshot(user, versionId).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest data snapshot for a user that has been saved in the database.
|
||||
* <p>
|
||||
* Not to be confused with {@link #getCurrentData(User)}, which will return the current data of a user
|
||||
* if they are online (this method will only return their latest <i>saved</i> snapshot).
|
||||
* </p>
|
||||
*
|
||||
* @param user The user to get the latest data snapshot of
|
||||
* @return The user's latest data snapshot, or an empty optional if the user has no data
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Optional<DataSnapshot.Unpacked>> getLatestSnapshot(@NotNull User user) {
|
||||
return plugin.supplyAsync(
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).map(snapshot -> snapshot.unpack(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit the latest data snapshot for a user
|
||||
*
|
||||
* @param user The user to edit the latest snapshot of
|
||||
* @param editor The editor function
|
||||
* @since 3.0
|
||||
*/
|
||||
public void editLatestSnapshot(@NotNull User user, @NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().getLatestSnapshot(user).ifPresent(snapshot -> {
|
||||
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
plugin.getDatabase().updateSnapshot(user, unpacked.pack(plugin));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a data snapshot to the database
|
||||
*
|
||||
* @param user The user to save the data for
|
||||
* @param snapshot The snapshot to save
|
||||
* @since 3.0
|
||||
*/
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an <i>existing</i> data snapshot in the database.
|
||||
* Not to be confused with {@link #addSnapshot(User, DataSnapshot)}, which will add a new snapshot if one
|
||||
* snapshot doesn't exist.
|
||||
*
|
||||
* @param user The user to update the snapshot of
|
||||
* @param snapshot The snapshot to update
|
||||
* @since 3.0
|
||||
*/
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().updateSnapshot(
|
||||
user, snapshot instanceof DataSnapshot.Unpacked unpacked
|
||||
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a data snapshot, preventing it from being rotated
|
||||
*
|
||||
* @param user The user to pin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to pin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void pinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().pinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a data snapshot, allowing it to be rotated
|
||||
*
|
||||
* @param user The user to unpin the snapshot of
|
||||
* @param snapshotVersion The version ID of the snapshot to unpin
|
||||
* @since 3.0
|
||||
*/
|
||||
public void unpinSnapshot(@NotNull User user, @NotNull UUID snapshotVersion) {
|
||||
plugin.runAsync(() -> plugin.getDatabase().unpinSnapshot(user, snapshotVersion));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param versionId The version ID of the snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot didn't exist)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull UUID versionId) {
|
||||
return plugin.supplyAsync(() -> plugin.getDatabase().deleteSnapshot(user, versionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a data snapshot from the database
|
||||
*
|
||||
* @param user The user to delete the snapshot of
|
||||
* @param snapshot The snapshot to delete
|
||||
* @return A future which will complete with true if the snapshot was deleted, or false if it wasn't
|
||||
* (e.g., if the snapshot hasn't been saved to the database yet)
|
||||
* @since 3.0
|
||||
*/
|
||||
public CompletableFuture<Boolean> deleteSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
|
||||
return deleteSnapshot(user, snapshot.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a new custom data type serializer.
|
||||
* <p>
|
||||
* This allows for custom {@link Data} types to be persisted in {@link DataSnapshot}s. To register
|
||||
* a new data type, you must provide a {@link Serializer} for serializing and deserializing the data type
|
||||
* and invoke this method.
|
||||
* </p>
|
||||
* You'll need to do this on every server you wish to sync data between. On servers where the registered
|
||||
* data type is not present, the data will be ignored and snapshots created on that server will not
|
||||
* contain the data.
|
||||
*
|
||||
* @param identifier The identifier of the data type to register.
|
||||
* Create one using {@code Identifier.from(Key.of("your_plugin_name", "key"))}
|
||||
* @param serializer An implementation of {@link Serializer} for serializing and deserializing the {@link Data}
|
||||
* @param <T> A type extending {@link Data}; this will represent the data being held.
|
||||
*/
|
||||
public <T extends Data> void registerDataSerializer(@NotNull Identifier identifier,
|
||||
@NotNull Serializer<T> serializer) {
|
||||
plugin.registerSerializer(identifier, serializer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param unpacked The unpacked snapshot
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed packSnapshot(@NotNull DataSnapshot.Unpacked unpacked) {
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @return The unpacked snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpackSnapshot(@NotNull DataSnapshot.Packed packed) {
|
||||
return packed.unpack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack, edit, and repack a data snapshot.
|
||||
* </p>
|
||||
* This won't save the snapshot to the database; it'll just edit the data snapshot in place.
|
||||
*
|
||||
* @param packed The packed snapshot
|
||||
* @param editor An editor function for editing the unpacked snapshot
|
||||
* @return The edited packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed editPackedSnapshot(@NotNull DataSnapshot.Packed packed,
|
||||
@NotNull ThrowingConsumer<DataSnapshot.Unpacked> editor) {
|
||||
final DataSnapshot.Unpacked unpacked = packed.unpack(plugin);
|
||||
editor.accept(unpacked);
|
||||
return unpacked.pack(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the estimated size of a {@link DataSnapshot} in bytes
|
||||
*
|
||||
* @param snapshot The snapshot to get the size of
|
||||
* @return The size of the snapshot in bytes
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getSnapshotFileSize(@NotNull DataSnapshot snapshot) {
|
||||
return (snapshot instanceof DataSnapshot.Packed packed)
|
||||
? packed.getFileSize(plugin)
|
||||
: ((DataSnapshot.Unpacked) snapshot).pack(plugin).getFileSize(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a builder for creating a new data snapshot
|
||||
*
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Builder snapshotBuilder() {
|
||||
return DataSnapshot.builder(plugin).saveCause(DataSnapshot.SaveCause.API);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize a JSON string to an {@link Adaptable}
|
||||
*
|
||||
* @param serialized The serialized JSON string
|
||||
* @param type The type of the element
|
||||
* @param <T> The type of the element
|
||||
* @return The deserialized element
|
||||
* @throws Serializer.DeserializationException If the element could not be deserialized
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> T deserializeData(@NotNull String serialized, Class<T> type)
|
||||
throws Serializer.DeserializationException {
|
||||
return plugin.getDataAdapter().fromJson(serialized, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an {@link Adaptable} to a JSON string
|
||||
*
|
||||
* @param element The element to serialize
|
||||
* @param <T> The type of the element
|
||||
* @return The serialized JSON string
|
||||
* @throws Serializer.SerializationException If the element could not be serialized
|
||||
*/
|
||||
@NotNull
|
||||
public <T extends Adaptable> String serializeData(@NotNull T element)
|
||||
throws Serializer.SerializationException {
|
||||
return plugin.getDataAdapter().toJson(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get the plugin instance
|
||||
*
|
||||
* @return The plugin instance
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
public HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* An exception indicating the plugin has been accessed before it has been registered.
|
||||
*/
|
||||
static final class NotRegisteredException extends IllegalStateException {
|
||||
|
||||
private static final String MESSAGE = """
|
||||
Could not access the HuskSync API as it has not yet been registered. This could be because:
|
||||
1) HuskSync has failed to enable successfully
|
||||
2) Your plugin isn't set to load after HuskSync has
|
||||
(Check if it set as a (soft)depend in plugin.yml or to load: BEFORE in paper-plugin.yml?)
|
||||
3) You are attempting to access HuskSync on plugin construction/before your plugin has enabled.
|
||||
4) You have shaded HuskSync into your plugin jar and need to fix your maven/gradle/build script
|
||||
to only include HuskSync as a dependency and not as a shaded dependency.""";
|
||||
|
||||
NotRegisteredException() {
|
||||
super(MESSAGE);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class Command extends Node {
|
||||
|
||||
private final String usage;
|
||||
private final Map<String, Boolean> additionalPermissions;
|
||||
|
||||
protected Command(@NotNull String name, @NotNull List<String> aliases, @NotNull String usage,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(name, aliases, plugin);
|
||||
this.usage = usage;
|
||||
this.additionalPermissions = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void onExecuted(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!executor.hasPermission(getPermission())) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> this.execute(executor, args));
|
||||
}
|
||||
|
||||
public abstract void execute(@NotNull CommandUser executor, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
public final String getRawUsage() {
|
||||
return usage;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final String getUsage() {
|
||||
return "/" + getName() + " " + getRawUsage();
|
||||
}
|
||||
|
||||
public final void addAdditionalPermissions(@NotNull Map<String, Boolean> permissions) {
|
||||
permissions.forEach((permission, value) -> this.additionalPermissions.put(getPermission(permission), value));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final Map<String, Boolean> getAdditionalPermissions() {
|
||||
return additionalPermissions;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(getName() + "_command_description")
|
||||
.orElse(getUsage());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public final HuskSync getPlugin() {
|
||||
return plugin;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an abstract cross-platform representation for a plugin command
|
||||
*/
|
||||
public abstract class CommandBase {
|
||||
|
||||
/**
|
||||
* The input string to match for this command
|
||||
*/
|
||||
public final String command;
|
||||
|
||||
/**
|
||||
* The permission node required to use this command
|
||||
*/
|
||||
public final String permission;
|
||||
|
||||
/**
|
||||
* Alias input strings for this command
|
||||
*/
|
||||
public final String[] aliases;
|
||||
|
||||
/**
|
||||
* Instance of the implementing plugin
|
||||
*/
|
||||
public final HuskSync plugin;
|
||||
|
||||
|
||||
public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
|
||||
this.command = command;
|
||||
this.permission = permission.node;
|
||||
this.plugin = implementor;
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the command is executed
|
||||
*
|
||||
* @param player {@link OnlineUser} executing the command
|
||||
* @param args Command arguments
|
||||
*/
|
||||
public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
|
||||
|
||||
/**
|
||||
* Returns the localised description string of this command
|
||||
*
|
||||
* @return the command description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(command + "_command_description")
|
||||
.orElse("A HuskHomes command");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Interface providing console execution of commands
|
||||
*/
|
||||
public interface ConsoleExecutable {
|
||||
|
||||
/**
|
||||
* What to do when console executes a command
|
||||
*
|
||||
* @param args command argument strings
|
||||
*/
|
||||
void onConsoleExecute(@NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -1,90 +1,92 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Optional;
|
||||
|
||||
public class EnderChestCommand extends CommandBase implements TabCompletable {
|
||||
public class EnderChestCommand extends ItemsCommand {
|
||||
|
||||
public EnderChestCommand(@NotNull HuskSync implementor) {
|
||||
super("enderchest", Permission.COMMAND_ENDER_CHEST, implementor, "echest", "openechest");
|
||||
public EnderChestCommand(@NotNull HuskSync plugin) {
|
||||
super(plugin, List.of("enderchest", "echest", "openechest"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length == 0 || args.length > 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/enderchest <player>")
|
||||
.ifPresent(player::sendMessage);
|
||||
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.EnderChest> optionalEnderChest = snapshot.getEnderChest();
|
||||
if (optionalEnderChest.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(user -> {
|
||||
if (args.length == 2) {
|
||||
// View user data by specified UUID
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[1]);
|
||||
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> showEnderChestMenu(player, userData, user, false),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/enderchest <player> [version_uuid]").ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
// View latest user data
|
||||
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
|
||||
versionedUserData -> showEnderChestMenu(player, versionedUserData, user, true),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage)));
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
final Data.Items.EnderChest enderChest = optionalEnderChest.get();
|
||||
viewer.showGui(
|
||||
enderChest,
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_menu_title", user.getUsername())
|
||||
.orElse(new MineDown(String.format("%s's Ender Chest", user.getUsername()))),
|
||||
allowEdit,
|
||||
enderChest.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !enderChest.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
|
||||
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, final boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
|
||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(enderChestDataOnClose -> {
|
||||
if (!menu.canEdit) {
|
||||
return;
|
||||
}
|
||||
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
|
||||
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
|
||||
data.getStatisticsData(), data.getLocationData(),
|
||||
data.getPersistentDataContainerData(),
|
||||
plugin.getMinecraftVersion().toString());
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
});
|
||||
});
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
// Creates a new snapshot with the updated enderChest
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and pack the snapshot with the updated enderChest
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
|
||||
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND));
|
||||
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
|
||||
});
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface Executable {
|
||||
|
||||
void onExecuted(@NotNull CommandUser executor, @NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -1,141 +1,184 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.william278.desertwell.about.AboutMenu;
|
||||
import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
|
||||
public class HuskSyncCommand extends Command implements TabProvider {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload", "migrate"};
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"about", false,
|
||||
"reload", true,
|
||||
"migrate", true,
|
||||
"update", true
|
||||
);
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync implementor) {
|
||||
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
|
||||
private final UpdateChecker updateChecker;
|
||||
private final AboutMenu aboutMenu;
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync plugin) {
|
||||
super("husksync", List.of(), "[" + String.join("|", SUB_COMMANDS.keySet()) + "]", plugin);
|
||||
addAdditionalPermissions(SUB_COMMANDS);
|
||||
|
||||
this.updateChecker = plugin.getUpdateChecker();
|
||||
this.aboutMenu = AboutMenu.builder()
|
||||
.title(Component.text("HuskSync"))
|
||||
.description(Component.text("A modern, cross-server player data synchronization system"))
|
||||
.version(plugin.getPluginVersion())
|
||||
.credits("Author",
|
||||
AboutMenu.Credit.of("William278").description("Click to visit website").url("https://william278.net"))
|
||||
.credits("Contributors",
|
||||
AboutMenu.Credit.of("HarvelsX").description("Code"),
|
||||
AboutMenu.Credit.of("HookWoods").description("Code"))
|
||||
.credits("Translators",
|
||||
AboutMenu.Credit.of("Namiu").description("Japanese (ja-jp)"),
|
||||
AboutMenu.Credit.of("anchelthe").description("Spanish (es-es)"),
|
||||
AboutMenu.Credit.of("Melonzio").description("Spanish (es-es)"),
|
||||
AboutMenu.Credit.of("Ceddix").description("German (de-de)"),
|
||||
AboutMenu.Credit.of("Pukejoy_1").description("Bulgarian (bg-bg)"),
|
||||
AboutMenu.Credit.of("mateusneresrb").description("Brazilian Portuguese (pt-br)"),
|
||||
AboutMenu.Credit.of("小蔡").description("Traditional Chinese (zh-tw)"),
|
||||
AboutMenu.Credit.of("Ghost-chu").description("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("DJelly4K").description("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("Thourgard").description("Ukrainian (uk-ua)"),
|
||||
AboutMenu.Credit.of("xF3d3").description("Italian (it-it)"))
|
||||
.buttons(
|
||||
AboutMenu.Link.of("https://william278.net/docs/husksync").text("Documentation").icon("⛏"),
|
||||
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").text("Issues").icon("❌").color(TextColor.color(0xff9f0f)),
|
||||
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").text("Discord").icon("⭐").color(TextColor.color(0x6773f5)))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
displayPluginInformation(player);
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
final String subCommand = parseStringArg(args, 0).orElse("about").toLowerCase(Locale.ENGLISH);
|
||||
if (SUB_COMMANDS.containsKey(subCommand) && !executor.hasPermission(getPermission(subCommand))) {
|
||||
plugin.getLocales().getLocale("error_no_permission")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter());
|
||||
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
|
||||
if (updateChecker.isUpdateAvailable(latestVersion)) {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + latestVersion + "](#00fb9a bold)" +
|
||||
"[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
|
||||
"[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
|
||||
} else {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + updateChecker.getCurrentVersion() + "](#00fb9a)"));
|
||||
}
|
||||
});
|
||||
}
|
||||
case "info", "about" -> displayPluginInformation(player);
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.reload();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(player::sendMessage);
|
||||
}
|
||||
case "migrate" ->
|
||||
plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage);
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/husksync <update/about/reload>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConsoleExecute(@NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" ->
|
||||
new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()).logToConsole();
|
||||
case "info", "about" ->
|
||||
plugin.getLoggingAdapter().log(Level.INFO, new MineDown(plugin.getLocales().stripMineDown(
|
||||
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString()))));
|
||||
switch (subCommand) {
|
||||
case "about" -> executor.sendMessage(aboutMenu.toComponent());
|
||||
case "reload" -> {
|
||||
plugin.reload();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files.");
|
||||
try {
|
||||
plugin.loadConfigs();
|
||||
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
executor.sendMessage(new MineDown(
|
||||
"[Error:](#ff3300) [Failed to reload the plugin. Check console for errors.](#ff7e5e)"
|
||||
));
|
||||
plugin.log(Level.SEVERE, "Failed to reload the plugin", e);
|
||||
}
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (args.length < 2) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
logMigratorsList();
|
||||
if (executor instanceof OnlineUser) {
|
||||
plugin.getLocales().getLocale("error_console_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream().filter(availableMigrator ->
|
||||
availableMigrator.getIdentifier().equalsIgnoreCase(args[1])).findFirst();
|
||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
||||
if (args.length < 3) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, migrator.getHelpMenu());
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.getLoggingAdapter().log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
||||
default -> plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Invalid syntax. Console usage: \"husksync migrate " + args[1] + " <start/set>");
|
||||
}
|
||||
}, () -> {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
logMigratorsList();
|
||||
});
|
||||
this.handleMigrationCommand(args);
|
||||
}
|
||||
default -> plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||
case "update" -> updateChecker.check().thenAccept(checked -> {
|
||||
if (checked.isUpToDate()) {
|
||||
plugin.getLocales().getLocale("up_to_date", plugin.getPluginVersion().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getLocales().getLocale("update_available", checked.getLatestVersion().toString(),
|
||||
plugin.getPluginVersion().toString()).ifPresent(executor::sendMessage);
|
||||
});
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private void logMigratorsList() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n" +
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
|
||||
.collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void displayPluginInformation(@NotNull OnlineUser player) {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
// Handle a migration console command input
|
||||
private void handleMigrationCommand(@NotNull String[] args) {
|
||||
if (args.length < 2) {
|
||||
plugin.log(Level.INFO,
|
||||
"Please choose a migrator, then run \"husksync migrate <migrator>\"");
|
||||
this.logMigratorList();
|
||||
return;
|
||||
}
|
||||
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString())));
|
||||
|
||||
final Optional<Migrator> selectedMigrator = plugin.getAvailableMigrators().stream()
|
||||
.filter(available -> available.getIdentifier().equalsIgnoreCase(args[1]))
|
||||
.findFirst();
|
||||
selectedMigrator.ifPresentOrElse(migrator -> {
|
||||
if (args.length < 3) {
|
||||
plugin.log(Level.INFO, migrator.getHelpMenu());
|
||||
return;
|
||||
}
|
||||
switch (args[2]) {
|
||||
case "start" -> migrator.start().thenAccept(succeeded -> {
|
||||
if (succeeded) {
|
||||
plugin.log(Level.INFO, "Migration completed successfully!");
|
||||
} else {
|
||||
plugin.log(Level.WARNING, "Migration failed!");
|
||||
}
|
||||
});
|
||||
case "set" -> migrator.handleConfigurationCommand(Arrays.copyOfRange(args, 3, args.length));
|
||||
default -> plugin.log(Level.INFO, String.format(
|
||||
"Invalid syntax. Console usage: \"husksync migrate %s <start/set>", args[1]
|
||||
));
|
||||
}
|
||||
}, () -> {
|
||||
plugin.log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
this.logMigratorList();
|
||||
});
|
||||
}
|
||||
|
||||
// Log the list of available migrators
|
||||
private void logMigratorList() {
|
||||
plugin.log(Level.INFO, String.format(
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n%s",
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> String.format("%s - %s", migrator.getIdentifier(), migrator.getName()))
|
||||
.collect(Collectors.joining("\n"))
|
||||
));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,88 +1,92 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Optional;
|
||||
|
||||
public class InventoryCommand extends CommandBase implements TabCompletable {
|
||||
public class InventoryCommand extends ItemsCommand {
|
||||
|
||||
public InventoryCommand(@NotNull HuskSync implementor) {
|
||||
super("inventory", Permission.COMMAND_INVENTORY, implementor, "invsee", "openinv");
|
||||
public InventoryCommand(@NotNull HuskSync plugin) {
|
||||
super(plugin, List.of("inventory", "invsee", "openinv"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length == 0 || args.length > 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/inventory <player>")
|
||||
.ifPresent(player::sendMessage);
|
||||
protected void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit) {
|
||||
final Optional<Data.Items.Inventory> optionalInventory = snapshot.getInventory();
|
||||
if (optionalInventory.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().getUserByName(args[0].toLowerCase()).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(user -> {
|
||||
if (args.length == 2) {
|
||||
// View user data by specified UUID
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[1]);
|
||||
plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> showInventoryMenu(player, userData, user, false),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/inventory <player> [version_uuid]").ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
// View latest user data
|
||||
plugin.getDatabase().getCurrentUserData(user).thenAccept(optionalData -> optionalData.ifPresentOrElse(
|
||||
versionedUserData -> showInventoryMenu(player, versionedUserData, user, true),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage)));
|
||||
|
||||
// Display opening message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", user.getUsername(),
|
||||
snapshot.getTimestamp().format(DateTimeFormatter.ofPattern("dd/MM/yyyy, HH:mm")))
|
||||
.ifPresent(viewer::sendMessage);
|
||||
|
||||
// Show GUI
|
||||
final Data.Items.Inventory inventory = optionalInventory.get();
|
||||
viewer.showGui(
|
||||
inventory,
|
||||
plugin.getLocales().getLocale("inventory_viewer_menu_title", user.getUsername())
|
||||
.orElse(new MineDown(String.format("%s's Inventory", user.getUsername()))),
|
||||
allowEdit,
|
||||
inventory.getSlotCount(),
|
||||
(itemsOnClose) -> {
|
||||
if (allowEdit && !inventory.equals(itemsOnClose)) {
|
||||
plugin.runAsync(() -> this.updateItems(viewer, itemsOnClose, user));
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage)));
|
||||
}
|
||||
|
||||
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
|
||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(inventoryDataOnClose -> {
|
||||
if (!menu.canEdit) {
|
||||
return;
|
||||
}
|
||||
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
|
||||
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
|
||||
data.getStatisticsData(), data.getLocationData(),
|
||||
data.getPersistentDataContainerData(),
|
||||
plugin.getMinecraftVersion().toString());
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
});
|
||||
});
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
// Creates a new snapshot with the updated inventory
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
|
||||
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
|
||||
if (latestData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and pack the snapshot with the updated inventory
|
||||
final DataSnapshot.Packed snapshot = latestData.get().copy();
|
||||
snapshot.edit(plugin, (data) -> {
|
||||
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
|
||||
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND));
|
||||
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
|
||||
});
|
||||
plugin.getDatabase().addSnapshot(user, snapshot);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class ItemsCommand extends Command implements TabProvider {
|
||||
|
||||
protected ItemsCommand(@NotNull HuskSync plugin, @NotNull List<String> aliases) {
|
||||
super(aliases.get(0), aliases.subList(1, aliases.size()), "<player> [version_uuid]", plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(Map.of("edit", true));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
if (!(executor instanceof OnlineUser player)) {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the user to view the items for
|
||||
final Optional<User> optionalUser = parseStringArg(args, 0)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale(
|
||||
args.length >= 1 ? "error_invalid_player" : "error_invalid_syntax", getUsage()
|
||||
).ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the user data
|
||||
final User user = optionalUser.get();
|
||||
parseUUIDArg(args, 1).ifPresentOrElse(
|
||||
version -> this.showSnapshotItems(player, user, version),
|
||||
() -> this.showLatestItems(player, user)
|
||||
);
|
||||
}
|
||||
|
||||
// View (and edit) the latest user data
|
||||
private void showLatestItems(@NotNull OnlineUser viewer, @NotNull User user) {
|
||||
plugin.getRedisManager().getUserData(user.getUuid(), user).thenAccept(data -> data
|
||||
.or(() -> plugin.getDatabase().getLatestSnapshot(user))
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user,
|
||||
viewer.hasPermission(getPermission("edit"))
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
));
|
||||
}
|
||||
|
||||
// View a specific version of the user data
|
||||
private void showSnapshotItems(@NotNull OnlineUser viewer, @NotNull User user, @NotNull UUID version) {
|
||||
plugin.getDatabase().getSnapshot(user, version)
|
||||
.ifPresentOrElse(
|
||||
snapshot -> this.showItems(
|
||||
viewer, snapshot.unpack(plugin), user, false
|
||||
),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(viewer::sendMessage)
|
||||
);
|
||||
}
|
||||
|
||||
// Show a GUI menu with the correct item data from the snapshot
|
||||
protected abstract void showItems(@NotNull OnlineUser viewer, @NotNull DataSnapshot.Unpacked snapshot,
|
||||
@NotNull User user, boolean allowEdit);
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
105
common/src/main/java/net/william278/husksync/command/Node.java
Normal file
105
common/src/main/java/net/william278/husksync/command/Node.java
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
|
||||
public abstract class Node implements Executable {
|
||||
|
||||
protected static final String PERMISSION_PREFIX = "husksync.command";
|
||||
|
||||
protected final HuskSync plugin;
|
||||
private final String name;
|
||||
private final List<String> aliases;
|
||||
private boolean operatorCommand = false;
|
||||
|
||||
protected Node(@NotNull String name, @NotNull List<String> aliases, @NotNull HuskSync plugin) {
|
||||
if (name.isBlank()) {
|
||||
throw new IllegalArgumentException("Command name cannot be blank");
|
||||
}
|
||||
this.name = name;
|
||||
this.aliases = aliases;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getAliases() {
|
||||
return aliases;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getPermission(@NotNull String... child) {
|
||||
final StringJoiner joiner = new StringJoiner(".")
|
||||
.add(PERMISSION_PREFIX)
|
||||
.add(getName());
|
||||
for (final String node : child) {
|
||||
joiner.add(node);
|
||||
}
|
||||
return joiner.toString().trim();
|
||||
}
|
||||
|
||||
public boolean isOperatorCommand() {
|
||||
return operatorCommand;
|
||||
}
|
||||
|
||||
public void setOperatorCommand(boolean operatorCommand) {
|
||||
this.operatorCommand = operatorCommand;
|
||||
}
|
||||
|
||||
protected Optional<String> parseStringArg(@NotNull String[] args, int index) {
|
||||
if (args.length > index) {
|
||||
return Optional.of(args[index]);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
protected Optional<Integer> parseIntArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(Integer.parseInt(arg));
|
||||
} catch (NumberFormatException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected Optional<UUID> parseUUIDArg(@NotNull String[] args, int index) {
|
||||
return parseStringArg(args, index).flatMap(arg -> {
|
||||
try {
|
||||
return Optional.of(UUID.fromString(arg));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Static plugin permission nodes required to execute commands
|
||||
*/
|
||||
public enum Permission {
|
||||
|
||||
/*
|
||||
* /husksync command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /husksync} command (subcommand permissions required)
|
||||
*/
|
||||
COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user view plugin info {@code /husksync info}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_INFO("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user reload the plugin {@code /husksync reload}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user view the plugin version and check for updates {@code /husksync update}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /userdata command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user view user data {@code /userdata view/list (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA("husksync.command.userdata", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user restore and delete user data {@code /userdata restore/delete (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /inventory command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /inventory (player)} command and view offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY("husksync.command.inventory", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' inventories
|
||||
*/
|
||||
COMMAND_INVENTORY_EDIT("husksync.command.inventory.edit", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /enderchest command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /enderchest (player)} command and view offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST("husksync.command.enderchest", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' ender chests
|
||||
*/
|
||||
COMMAND_ENDER_CHEST_EDIT("husksync.command.enderchest.edit", DefaultAccess.OPERATORS);
|
||||
|
||||
|
||||
public final String node;
|
||||
public final DefaultAccess defaultAccess;
|
||||
|
||||
Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
|
||||
this.node = node;
|
||||
this.defaultAccess = defaultAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies who gets what permissions by default
|
||||
*/
|
||||
public enum DefaultAccess {
|
||||
/**
|
||||
* Everyone gets this permission node by default
|
||||
*/
|
||||
EVERYONE,
|
||||
/**
|
||||
* Nobody gets this permission node by default
|
||||
*/
|
||||
NOBODY,
|
||||
/**
|
||||
* Server operators ({@code /op}) get this permission node by default
|
||||
*/
|
||||
OPERATORS
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface providing tab completions for a command
|
||||
*/
|
||||
public interface TabCompletable {
|
||||
|
||||
/**
|
||||
* What should be returned when the player or console attempts to TAB-complete a command
|
||||
*
|
||||
* @param args Current command arguments
|
||||
* @return List of String arguments to offer TAB suggestions
|
||||
*/
|
||||
List<String> onTabComplete(@NotNull String[] args);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TabProvider {
|
||||
|
||||
@Nullable
|
||||
List<String> suggest(@NotNull CommandUser user, @NotNull String[] args);
|
||||
|
||||
@NotNull
|
||||
default List<String> getSuggestions(@NotNull CommandUser user, @NotNull String[] args) {
|
||||
List<String> suggestions = suggest(user, args);
|
||||
if (suggestions == null) {
|
||||
suggestions = List.of();
|
||||
}
|
||||
return filter(suggestions, args);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default List<String> filter(@NotNull List<String> suggestions, @NotNull String[] args) {
|
||||
return suggestions.stream()
|
||||
.filter(suggestion -> args.length == 0 || suggestion.toLowerCase()
|
||||
.startsWith(args[args.length - 1].toLowerCase().trim()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,239 +1,235 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.CommandUser;
|
||||
import net.william278.husksync.user.User;
|
||||
import net.william278.husksync.util.DataDumper;
|
||||
import net.william278.husksync.util.DataSnapshotList;
|
||||
import net.william278.husksync.util.DataSnapshotOverview;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
public class UserDataCommand extends Command implements TabProvider {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin"};
|
||||
private static final Map<String, Boolean> SUB_COMMANDS = Map.of(
|
||||
"view", false,
|
||||
"list", false,
|
||||
"delete", true,
|
||||
"restore", true,
|
||||
"pin", true,
|
||||
"dump", true
|
||||
);
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync implementor) {
|
||||
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
|
||||
public UserDataCommand(@NotNull HuskSync plugin) {
|
||||
super("userdata", List.of("playerdata"), String.format(
|
||||
"<%s> [username] [version_uuid]", String.join("/", SUB_COMMANDS.keySet())
|
||||
), plugin);
|
||||
setOperatorCommand(true);
|
||||
addAdditionalPermissions(SUB_COMMANDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata <view/list/delete/restore/pin> <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
public void execute(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
final String subCommand = parseStringArg(args, 0).orElse("view").toLowerCase(Locale.ENGLISH);
|
||||
final Optional<User> optionalUser = parseStringArg(args, 1)
|
||||
.flatMap(name -> plugin.getDatabase().getUserByName(name))
|
||||
.or(() -> parseStringArg(args, 0).flatMap(name -> plugin.getDatabase().getUserByName(name)))
|
||||
.or(() -> args.length < 2 && executor instanceof User userExecutor
|
||||
? Optional.of(userExecutor) : Optional.empty());
|
||||
final Optional<UUID> optionalUuid = parseUUIDArg(args, 2).or(() -> parseUUIDArg(args, 1));
|
||||
if (optionalUser.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "view" -> {
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data ->
|
||||
data.ifPresentOrElse(userData -> plugin.getDataEditor()
|
||||
.displayDataOverview(player, userData, user),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getCurrentUserData(user).thenAccept(
|
||||
latestData -> latestData.ifPresentOrElse(
|
||||
userData -> plugin.getDataEditor()
|
||||
.displayDataOverview(player, userData, user),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
}
|
||||
}
|
||||
final User user = optionalUser.get();
|
||||
switch (subCommand) {
|
||||
case "view" -> optionalUuid.ifPresentOrElse(
|
||||
// Show the specified snapshot
|
||||
version -> plugin.getDatabase().getSnapshot(user, version).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage)),
|
||||
|
||||
// Show the latest snapshot
|
||||
() -> plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
|
||||
data -> DataSnapshotOverview.of(
|
||||
data.unpack(plugin), data.getFileSize(plugin), user, plugin
|
||||
).show(executor),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage))
|
||||
);
|
||||
|
||||
case "list" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
// Check if there is data to display
|
||||
final List<DataSnapshot.Packed> dataList = plugin.getDatabase().getAllSnapshots(user);
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata list <username>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> {
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDataEditor().displayDataList(player, dataList, user);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
|
||||
// Show the list to the player
|
||||
DataSnapshotList.create(dataList, user, plugin).displayPage(
|
||||
executor,
|
||||
parseIntArg(args, 2).or(() -> parseIntArg(args, 1)).orElse(1)
|
||||
);
|
||||
}
|
||||
|
||||
case "delete" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete user data by specified UUID
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
final UUID version = optionalUuid.get();
|
||||
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> {
|
||||
if (deleted) {
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata delete <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
version.toString().split("-")[0],
|
||||
version.toString(),
|
||||
user.getUsername(),
|
||||
user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "restore" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
// Get user data by specified uuid and username
|
||||
if (args.length < 3) {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> {
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().setUserData(user, data.get().userData(),
|
||||
DataSaveCause.BACKUP_RESTORE);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join();
|
||||
plugin.getLocales().getLocale("data_restored",
|
||||
user.username,
|
||||
user.uuid.toString(),
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata restore <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
|
||||
// Restore user data by specified UUID
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <=0 health)
|
||||
final DataSnapshot.Packed data = optionalData.get().copy();
|
||||
data.edit(plugin, (unpacked -> {
|
||||
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
|
||||
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
|
||||
unpacked.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE));
|
||||
}));
|
||||
|
||||
// Set the user's data and send a message
|
||||
plugin.getDatabase().addSnapshot(user, data);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data);
|
||||
plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(),
|
||||
data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "pin" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
|
||||
// Check that the data exists
|
||||
final Optional<DataSnapshot.Packed> optionalData = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (optionalData.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Pin or unpin the data
|
||||
final DataSnapshot.Packed data = optionalData.get();
|
||||
if (data.isPinned()) {
|
||||
plugin.getDatabase().unpinSnapshot(user, data.getId());
|
||||
} else {
|
||||
plugin.getDatabase().pinSnapshot(user, data.getId());
|
||||
}
|
||||
plugin.getLocales().getLocale(data.isPinned() ? "data_unpinned" : "data_pinned", data.getShortId(),
|
||||
data.getId().toString(), user.getUsername(), user.getUuid().toString())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
|
||||
case "dump" -> {
|
||||
if (optionalUuid.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine dump type
|
||||
final boolean webDump = parseStringArg(args, 3)
|
||||
.map(arg -> arg.equalsIgnoreCase("web"))
|
||||
.orElse(false);
|
||||
final Optional<DataSnapshot.Packed> data = plugin.getDatabase().getSnapshot(user, optionalUuid.get());
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(executor::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Dump the data
|
||||
final DataSnapshot.Packed userData = data.get();
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||
if (userData.pinned()) {
|
||||
plugin.getDatabase().unpinUserData(user, versionUuid).join();
|
||||
plugin.getLocales().getLocale("data_unpinned",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
} else {
|
||||
plugin.getDatabase().pinUserData(user, versionUuid).join();
|
||||
plugin.getLocales().getLocale("data_pinned",
|
||||
versionUuid.toString().split("-")[0],
|
||||
versionUuid.toString(),
|
||||
user.username,
|
||||
user.uuid.toString())
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata pin <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getLocales().getLocale("data_dumped", userData.getShortId(), user.getUsername(),
|
||||
(webDump ? dumper.toWeb() : dumper.toFile())).ifPresent(executor::sendMessage);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}
|
||||
|
||||
default -> plugin.getLocales().getLocale("error_invalid_syntax", getUsage())
|
||||
.ifPresent(executor::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
switch (args.length) {
|
||||
case 0, 1 -> {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
case 2 -> {
|
||||
return plugin.getOnlineUsers().stream().map(user -> user.username)
|
||||
.filter(argument -> argument.startsWith(args[1]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
public List<String> suggest(@NotNull CommandUser executor, @NotNull String[] args) {
|
||||
return switch (args.length) {
|
||||
case 0, 1 -> SUB_COMMANDS.keySet().stream().sorted().toList();
|
||||
case 2 -> plugin.getOnlineUsers().stream().map(User::getUsername).toList();
|
||||
case 4 -> parseStringArg(args, 0)
|
||||
.map(arg -> arg.equalsIgnoreCase("dump") ? List.of("web", "file") : null)
|
||||
.orElse(null);
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,57 +1,71 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.annotaml.YamlFile;
|
||||
import net.william278.paginedown.ListOptions;
|
||||
import org.apache.commons.text.StringEscapeUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Loaded locales used by the plugin to display various locales
|
||||
* Loaded locales used by the plugin to display styled messages
|
||||
*/
|
||||
@YamlFile(rootedMap = true, header = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync Locales ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ See plugin about menu for international locale credits
|
||||
┣╸ Formatted in MineDown: https://github.com/Phoenix616/MineDown
|
||||
┗╸ Translate HuskSync: https://william278.net/docs/husksync/Translations""")
|
||||
public class Locales {
|
||||
|
||||
public static final String PLUGIN_INFORMATION = """
|
||||
[HuskSync](#00fb9a bold) [| Version %version%](#00fb9a)
|
||||
[A modern, cross-server player data synchronization system](gray)
|
||||
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
|
||||
[• Contributors:](white) [HarvelsX](gray show_text=&7Code), [HookWoods](gray show_text=&7Code)
|
||||
[• Translators:](white) [Namiu](gray show_text=&7\\(うにたろう\\) - Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Melonzio](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [mateusneresrb](gray show_text=&7Brazilian Portuguese, pt-br], [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [DJelly4K](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua), [xF3d3](gray show_text=&7Italian, it-it)
|
||||
[• Documentation:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://william278.net/docs/husksync/Home/)
|
||||
[• Bug reporting:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
|
||||
[• Discord support:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
|
||||
|
||||
/**
|
||||
* The raw set of locales loaded from yaml
|
||||
*/
|
||||
@NotNull
|
||||
private final HashMap<String, String> rawLocales;
|
||||
|
||||
private Locales(@NotNull YamlDocument localesConfig) {
|
||||
this.rawLocales = new HashMap<>();
|
||||
for (String localeId : localesConfig.getRoutesAsStrings(false)) {
|
||||
rawLocales.put(localeId, localesConfig.getString(localeId));
|
||||
}
|
||||
}
|
||||
public Map<String, String> rawLocales = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Returns an un-formatted locale loaded from the locales file
|
||||
* Returns a raw, unformatted locale loaded from the Locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId) {
|
||||
if (rawLocales.containsKey(localeId)) {
|
||||
return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n"));
|
||||
}
|
||||
return Optional.empty();
|
||||
return Optional.ofNullable(rawLocales.get(localeId)).map(StringEscapeUtils::unescapeJava);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an un-formatted locale loaded from the locales file, with replacements applied
|
||||
* Returns a raw, unformatted locale loaded from the Locales file, with replacements applied
|
||||
* <p>
|
||||
* Note that replacements will not be MineDown-escaped; use {@link #escapeMineDown(String)} to escape replacements
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
@@ -59,7 +73,7 @@ public class Locales {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file
|
||||
* Returns a MineDown-formatted locale from the Locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
|
||||
@@ -69,71 +83,131 @@ public class Locales {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file, with replacements applied
|
||||
* Returns a MineDown-formatted locale from the Locales file, with replacements applied
|
||||
* <p>
|
||||
* Note that replacements will be MineDown-escaped before application
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId, replacements).map(MineDown::new);
|
||||
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeMineDown)
|
||||
.toArray(String[]::new)).map(MineDown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply placeholder replacements to a raw locale
|
||||
*
|
||||
* @param rawLocale The raw, unparsed locale
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @param replacements An ordered array of replacement strings to fill in placeholders with
|
||||
* @return the raw locale, with inserted placeholders
|
||||
*/
|
||||
@NotNull
|
||||
private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
|
||||
int replacementIndexer = 1;
|
||||
for (String replacement : replacements) {
|
||||
String replacementString = "%" + replacementIndexer + "%";
|
||||
rawLocale = rawLocale.replace(replacementString, replacement);
|
||||
replacementIndexer = replacementIndexer + 1;
|
||||
replacementIndexer += 1;
|
||||
}
|
||||
return rawLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the locales from a BoostedYaml {@link YamlDocument} locales file
|
||||
* Escape a string from {@link MineDown} formatting for use in a MineDown-formatted locale
|
||||
* <p>
|
||||
* Although MineDown provides {@link MineDown#escape(String)}, that method fails to escape events
|
||||
* properly when using the escaped string in a replacement, so this is used instead
|
||||
*
|
||||
* @param localesConfig The loaded {@link YamlDocument} locales.yml file
|
||||
* @return the loaded {@link Locales}
|
||||
* @param string The string to escape
|
||||
* @return The escaped string
|
||||
*/
|
||||
public static Locales load(@NotNull YamlDocument localesConfig) {
|
||||
return new Locales(localesConfig);
|
||||
@NotNull
|
||||
public static String escapeMineDown(@NotNull String string) {
|
||||
final StringBuilder value = new StringBuilder();
|
||||
for (int i = 0; i < string.length(); ++i) {
|
||||
char c = string.charAt(i);
|
||||
boolean isEscape = c == '\\';
|
||||
boolean isColorCode = i + 1 < string.length() && (c == 167 || c == '&');
|
||||
boolean isEvent = c == '[' || c == ']' || c == '(' || c == ')';
|
||||
if (isEscape || isColorCode || isEvent) {
|
||||
value.append('\\');
|
||||
}
|
||||
|
||||
value.append(c);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a string of basic MineDown formatting, used for displaying plugin info to console
|
||||
* Truncates a String to a specified length, and appends an ellipsis if it is longer than the specified length
|
||||
*
|
||||
* @param string The string to strip
|
||||
* @return The MineDown-stripped string
|
||||
* @param string The string to truncate
|
||||
* @param length The maximum length of the string
|
||||
* @return The truncated string
|
||||
*/
|
||||
public String stripMineDown(@NotNull String string) {
|
||||
final String[] in = string.split("\n");
|
||||
final StringBuilder out = new StringBuilder();
|
||||
String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)";
|
||||
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher m = pattern.matcher(in[i]);
|
||||
|
||||
if (m.find()) {
|
||||
out.append(in[i].replace(m.group(0), ""));
|
||||
out.append(m.group(2));
|
||||
} else {
|
||||
out.append(in[i]);
|
||||
}
|
||||
|
||||
if (i + 1 != in.length) {
|
||||
out.append("\n");
|
||||
}
|
||||
@NotNull
|
||||
public static String truncate(@NotNull String string, int length) {
|
||||
if (string.length() > length) {
|
||||
return string.substring(0, length) + "…";
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
return string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base list options to use for a paginated chat list
|
||||
*
|
||||
* @param itemsPerPage The number of items to display per page
|
||||
* @return The list options
|
||||
*/
|
||||
@NotNull
|
||||
public ListOptions.Builder getBaseChatList(int itemsPerPage) {
|
||||
return new ListOptions.Builder()
|
||||
.setFooterFormat(getRawLocale("list_footer",
|
||||
"%previous_page_button%", "%current_page%",
|
||||
"%total_pages%", "%next_page_button%", "%page_jumpers%").orElse(""))
|
||||
.setNextButtonFormat(getRawLocale("list_next_page_button",
|
||||
"%next_page_index%", "%command%").orElse(""))
|
||||
.setPreviousButtonFormat(getRawLocale("list_previous_page_button",
|
||||
"%previous_page_index%", "%command%").orElse(""))
|
||||
.setPageJumpersFormat(getRawLocale("list_page_jumpers",
|
||||
"%page_jump_buttons%").orElse(""))
|
||||
.setPageJumperPageFormat(getRawLocale("list_page_jumper_button",
|
||||
"%target_page_index%", "%command%").orElse(""))
|
||||
.setPageJumperCurrentPageFormat(getRawLocale("list_page_jumper_current_page",
|
||||
"%current_page%").orElse(""))
|
||||
.setPageJumperPageSeparator(getRawLocale("list_page_jumper_separator").orElse(""))
|
||||
.setPageJumperGroupSeparator(getRawLocale("list_page_jumper_group_separator").orElse(""))
|
||||
.setItemsPerPage(itemsPerPage)
|
||||
.setEscapeItemsMineDown(false)
|
||||
.setSpaceAfterHeader(false)
|
||||
.setSpaceBeforeFooter(false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Locales() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the slot a system notification should be displayed in
|
||||
*/
|
||||
public enum NotificationSlot {
|
||||
/**
|
||||
* Displays the notification in the action bar
|
||||
*/
|
||||
ACTION_BAR,
|
||||
/**
|
||||
* Displays the notification in the chat
|
||||
*/
|
||||
CHAT,
|
||||
/**
|
||||
* Displays the notification in an Advancement Toast
|
||||
*/
|
||||
TOAST,
|
||||
/**
|
||||
* Does not display the notification
|
||||
*/
|
||||
NONE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,272 +1,416 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import net.william278.annotaml.YamlComment;
|
||||
import net.william278.annotaml.YamlFile;
|
||||
import net.william278.annotaml.YamlKey;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Settings used for the plugin, as read from the config file
|
||||
* Plugin settings, read from config.yml
|
||||
*/
|
||||
@YamlFile(header = """
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ HuskSync Config ┃
|
||||
┃ Developed by William278 ┃
|
||||
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
┣╸ Information: https://william278.net/project/husksync
|
||||
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
|
||||
┗╸ Documentation: https://william278.net/docs/husksync""")
|
||||
public class Settings {
|
||||
|
||||
/**
|
||||
* Map of {@link ConfigOption}s read from the config file
|
||||
*/
|
||||
private final HashMap<ConfigOption, Object> configOptions;
|
||||
// Top-level settings
|
||||
@YamlComment("Locale of the default language file to use. Docs: https://william278.net/docs/huskhomes/translations")
|
||||
@YamlKey("language")
|
||||
private String language = "en-gb";
|
||||
|
||||
// Load the settings from the document
|
||||
private Settings(@NotNull YamlDocument config) {
|
||||
this.configOptions = new HashMap<>();
|
||||
Arrays.stream(ConfigOption.values()).forEach(configOption -> configOptions
|
||||
.put(configOption, switch (configOption.optionType) {
|
||||
case BOOLEAN -> configOption.getBooleanValue(config);
|
||||
case STRING -> configOption.getStringValue(config);
|
||||
case DOUBLE -> configOption.getDoubleValue(config);
|
||||
case FLOAT -> configOption.getFloatValue(config);
|
||||
case INTEGER -> configOption.getIntValue(config);
|
||||
case STRING_LIST -> configOption.getStringListValue(config);
|
||||
}));
|
||||
}
|
||||
@YamlComment("Whether to automatically check for plugin updates on startup")
|
||||
@YamlKey("check_for_updates")
|
||||
private boolean checkForUpdates = true;
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a boolean
|
||||
* @throws ClassCastException if the option is not a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Boolean) configOptions.get(option);
|
||||
}
|
||||
@YamlComment("Specify a common ID for grouping servers running HuskSync. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("cluster_id")
|
||||
private String clusterId = "";
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string
|
||||
* @throws ClassCastException if the option is not a string
|
||||
*/
|
||||
public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (String) configOptions.get(option);
|
||||
}
|
||||
@YamlComment("Enable development debug logging")
|
||||
@YamlKey("debug_logging")
|
||||
private boolean debugLogging = false;
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a double
|
||||
* @throws ClassCastException if the option is not a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Double) configOptions.get(option);
|
||||
}
|
||||
@YamlComment("Whether to provide modern, rich TAB suggestions for commands (if available)")
|
||||
@YamlKey("brigadier_tab_completion")
|
||||
private boolean brigadierTabCompletion = false;
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a float
|
||||
* @throws ClassCastException if the option is not a float
|
||||
*/
|
||||
public double getFloatValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Float) configOptions.get(option);
|
||||
}
|
||||
@YamlComment("Whether to enable the Player Analytics hook. Docs: https://william278.net/docs/husksync/plan-hook")
|
||||
@YamlKey("enable_plan_hook")
|
||||
private boolean enablePlanHook = true;
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as an integer
|
||||
* @throws ClassCastException if the option is not an integer
|
||||
*/
|
||||
public int getIntegerValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Integer) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string {@link List}
|
||||
* @throws ClassCastException if the option is not a string list
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getStringListValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (List<String>) configOptions.get(option);
|
||||
// Database settings
|
||||
@YamlComment("Type of database to use (MYSQL, MARIADB)")
|
||||
@YamlKey("database.type")
|
||||
private Database.Type databaseType = Database.Type.MYSQL;
|
||||
|
||||
@YamlComment("Specify credentials here for your MYSQL or MARIADB database")
|
||||
@YamlKey("database.credentials.host")
|
||||
private String mySqlHost = "localhost";
|
||||
|
||||
@YamlKey("database.credentials.port")
|
||||
private int mySqlPort = 3306;
|
||||
|
||||
@YamlKey("database.credentials.database")
|
||||
private String mySqlDatabase = "HuskSync";
|
||||
|
||||
@YamlKey("database.credentials.username")
|
||||
private String mySqlUsername = "root";
|
||||
|
||||
@YamlKey("database.credentials.password")
|
||||
private String mySqlPassword = "pa55w0rd";
|
||||
|
||||
@YamlKey("database.credentials.parameters")
|
||||
private String mySqlConnectionParameters = "?autoReconnect=true"
|
||||
+ "&useSSL=false"
|
||||
+ "&useUnicode=true"
|
||||
+ "&characterEncoding=UTF-8";
|
||||
|
||||
@YamlComment("MYSQL / MARIADB database Hikari connection pool properties. "
|
||||
+ "Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("database.connection_pool.maximum_pool_size")
|
||||
private int mySqlConnectionPoolSize = 10;
|
||||
|
||||
@YamlKey("database.connection_pool.minimum_idle")
|
||||
private int mySqlConnectionPoolIdle = 10;
|
||||
|
||||
@YamlKey("database.connection_pool.maximum_lifetime")
|
||||
private long mySqlConnectionPoolLifetime = 1800000;
|
||||
|
||||
@YamlKey("database.connection_pool.keepalive_time")
|
||||
private long mySqlConnectionPoolKeepAlive = 0;
|
||||
|
||||
@YamlKey("database.connection_pool.connection_timeout")
|
||||
private long mySqlConnectionPoolTimeout = 5000;
|
||||
|
||||
@YamlComment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
|
||||
@YamlKey("database.table_names")
|
||||
private Map<String, String> tableNames = TableName.getDefaults();
|
||||
|
||||
|
||||
// Redis settings
|
||||
@YamlComment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
|
||||
@YamlKey("redis.credentials.host")
|
||||
private String redisHost = "localhost";
|
||||
|
||||
@YamlKey("redis.credentials.port")
|
||||
private int redisPort = 6379;
|
||||
|
||||
@YamlKey("redis.credentials.password")
|
||||
private String redisPassword = "";
|
||||
|
||||
@YamlKey("redis.use_ssl")
|
||||
private boolean redisUseSsl = false;
|
||||
|
||||
|
||||
// Synchronization settings
|
||||
@YamlComment("The number of data snapshot backups that should be kept at once per user")
|
||||
@YamlKey("synchronization.max_user_data_snapshots")
|
||||
private int maxUserDataSnapshots = 16;
|
||||
|
||||
@YamlComment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
|
||||
@YamlKey("synchronization.snapshot_backup_frequency")
|
||||
private int snapshotBackupFrequency = 4;
|
||||
|
||||
@YamlComment("List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated)."
|
||||
+ " Docs: https://william278.net/docs/husksync/data-rotation#save-causes")
|
||||
@YamlKey("synchronization.auto_pinned_save_causes")
|
||||
private List<String> autoPinnedSaveCauses = List.of(
|
||||
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
|
||||
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
|
||||
DataSnapshot.SaveCause.CONVERTED_FROM_V2.name(),
|
||||
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
|
||||
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
|
||||
);
|
||||
|
||||
@YamlComment("Whether to create a snapshot for users on a world when the server saves that world")
|
||||
@YamlKey("synchronization.save_on_world_save")
|
||||
private boolean saveOnWorldSave = true;
|
||||
|
||||
@YamlComment("Whether to create a snapshot for users when they die (containing their death drops)")
|
||||
@YamlKey("synchronization.save_on_death")
|
||||
private boolean saveOnDeath = false;
|
||||
|
||||
@YamlComment("Whether to save empty death drops for users when they die")
|
||||
@YamlKey("synchronization.save_empty_drops_on_death")
|
||||
private boolean saveEmptyDropsOnDeath = true;
|
||||
|
||||
@YamlComment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
|
||||
@YamlKey("synchronization.compress_data")
|
||||
private boolean compressData = true;
|
||||
|
||||
@YamlComment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
|
||||
@YamlKey("synchronization.notification_display_slot")
|
||||
private Locales.NotificationSlot notificationSlot = Locales.NotificationSlot.ACTION_BAR;
|
||||
|
||||
@YamlComment("(Experimental) Persist Cartography Table locked maps to let them be viewed on any server")
|
||||
@YamlKey("synchronization.persist_locked_maps")
|
||||
private boolean persistLockedMaps = true;
|
||||
|
||||
@YamlComment("Whether to synchronize player max health (requires health syncing to be enabled)")
|
||||
@YamlKey("synchronization.synchronize_max_health")
|
||||
private boolean synchronizeMaxHealth = true;
|
||||
|
||||
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved. "
|
||||
+ "You may need to modify this if you're using the keepInventory gamerule.")
|
||||
@YamlKey("synchronization.synchronize_dead_players_changing_server")
|
||||
private boolean synchronizeDeadPlayersChangingServer = true;
|
||||
|
||||
@YamlComment("How long, in milliseconds, this server should wait for a response from the redis server before "
|
||||
+ "pulling data from the database instead (i.e., if the user did not change servers).")
|
||||
@YamlKey("synchronization.network_latency_milliseconds")
|
||||
private int networkLatencyMilliseconds = 500;
|
||||
|
||||
@YamlComment("Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)")
|
||||
@YamlKey("synchronization.features")
|
||||
private Map<String, Boolean> synchronizationFeatures = Identifier.getConfigMap();
|
||||
|
||||
@YamlComment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
|
||||
@YamlKey("synchronization.blacklisted_commands_while_locked")
|
||||
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
|
||||
|
||||
@YamlComment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
|
||||
@YamlKey("synchronization.event_priorities")
|
||||
private Map<String, String> syncEventPriorities = EventListener.ListenerType.getDefaults();
|
||||
|
||||
|
||||
// Zero-args constructor for instantiation via Annotaml
|
||||
@SuppressWarnings("unused")
|
||||
public Settings() {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the settings from a BoostedYaml {@link YamlDocument} config file
|
||||
*
|
||||
* @param config The loaded {@link YamlDocument} config.yml file
|
||||
* @return the loaded {@link Settings}
|
||||
*/
|
||||
public static Settings load(@NotNull YamlDocument config) {
|
||||
return new Settings(config);
|
||||
@NotNull
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
public boolean doCheckForUpdates() {
|
||||
return checkForUpdates;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getClusterId() {
|
||||
return clusterId;
|
||||
}
|
||||
|
||||
public boolean doDebugLogging() {
|
||||
return debugLogging;
|
||||
}
|
||||
|
||||
public boolean doBrigadierTabCompletion() {
|
||||
return brigadierTabCompletion;
|
||||
}
|
||||
|
||||
public boolean usePlanHook() {
|
||||
return enablePlanHook;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Database.Type getDatabaseType() {
|
||||
return databaseType;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlHost() {
|
||||
return mySqlHost;
|
||||
}
|
||||
|
||||
public int getMySqlPort() {
|
||||
return mySqlPort;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlDatabase() {
|
||||
return mySqlDatabase;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlUsername() {
|
||||
return mySqlUsername;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlPassword() {
|
||||
return mySqlPassword;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMySqlConnectionParameters() {
|
||||
return mySqlConnectionParameters;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getTableName(@NotNull TableName tableName) {
|
||||
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.defaultName);
|
||||
}
|
||||
|
||||
public int getMySqlConnectionPoolSize() {
|
||||
return mySqlConnectionPoolSize;
|
||||
}
|
||||
|
||||
public int getMySqlConnectionPoolIdle() {
|
||||
return mySqlConnectionPoolIdle;
|
||||
}
|
||||
|
||||
public long getMySqlConnectionPoolLifetime() {
|
||||
return mySqlConnectionPoolLifetime;
|
||||
}
|
||||
|
||||
public long getMySqlConnectionPoolKeepAlive() {
|
||||
return mySqlConnectionPoolKeepAlive;
|
||||
}
|
||||
|
||||
public long getMySqlConnectionPoolTimeout() {
|
||||
return mySqlConnectionPoolTimeout;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRedisHost() {
|
||||
return redisHost;
|
||||
}
|
||||
|
||||
public int getRedisPort() {
|
||||
return redisPort;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getRedisPassword() {
|
||||
return redisPassword;
|
||||
}
|
||||
|
||||
public boolean redisUseSsl() {
|
||||
return redisUseSsl;
|
||||
}
|
||||
|
||||
public int getMaxUserDataSnapshots() {
|
||||
return maxUserDataSnapshots;
|
||||
}
|
||||
|
||||
public int getBackupFrequency() {
|
||||
return snapshotBackupFrequency;
|
||||
}
|
||||
|
||||
public boolean doSaveOnWorldSave() {
|
||||
return saveOnWorldSave;
|
||||
}
|
||||
|
||||
public boolean doSaveOnDeath() {
|
||||
return saveOnDeath;
|
||||
}
|
||||
|
||||
public boolean doSaveEmptyDropsOnDeath() {
|
||||
return saveEmptyDropsOnDeath;
|
||||
}
|
||||
|
||||
public boolean doCompressData() {
|
||||
return compressData;
|
||||
}
|
||||
|
||||
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
|
||||
return autoPinnedSaveCauses.contains(cause.name());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Locales.NotificationSlot getNotificationDisplaySlot() {
|
||||
return notificationSlot;
|
||||
}
|
||||
|
||||
public boolean doPersistLockedMaps() {
|
||||
return persistLockedMaps;
|
||||
}
|
||||
|
||||
public boolean doSynchronizeDeadPlayersChangingServer() {
|
||||
return synchronizeDeadPlayersChangingServer;
|
||||
}
|
||||
|
||||
public boolean doSynchronizeMaxHealth() {
|
||||
return synchronizeMaxHealth;
|
||||
}
|
||||
|
||||
public int getNetworkLatencyMilliseconds() {
|
||||
return networkLatencyMilliseconds;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public Map<String, Boolean> getSynchronizationFeatures() {
|
||||
return synchronizationFeatures;
|
||||
}
|
||||
|
||||
public boolean isSyncFeatureEnabled(@NotNull Identifier id) {
|
||||
return id.isCustom() || getSynchronizationFeatures().getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public List<String> getBlacklistedCommandsWhileLocked() {
|
||||
return blacklistedCommandsWhileLocked;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
|
||||
try {
|
||||
return EventListener.Priority.valueOf(syncEventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return EventListener.Priority.NORMAL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an option stored by a path in config.yml
|
||||
* Represents the names of tables in the database
|
||||
*/
|
||||
public enum ConfigOption {
|
||||
LANGUAGE("language", OptionType.STRING, "en-gb"),
|
||||
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
|
||||
public enum TableName {
|
||||
USERS("husksync_users"),
|
||||
USER_DATA("husksync_user_data");
|
||||
|
||||
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
|
||||
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false),
|
||||
private final String defaultName;
|
||||
|
||||
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
|
||||
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
|
||||
DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"),
|
||||
DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"),
|
||||
DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"),
|
||||
DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"),
|
||||
DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10),
|
||||
DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10),
|
||||
DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000),
|
||||
DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0),
|
||||
DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000),
|
||||
DATABASE_USERS_TABLE_NAME("database.table_names.users_table", OptionType.STRING, "husksync_users"),
|
||||
DATABASE_USER_DATA_TABLE_NAME("database.table_names.user_data_table", OptionType.STRING, "husksync_user_data"),
|
||||
TableName(@NotNull String defaultName) {
|
||||
this.defaultName = defaultName;
|
||||
}
|
||||
|
||||
REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"),
|
||||
REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379),
|
||||
REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
|
||||
REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
|
||||
|
||||
SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS("synchronization.max_user_data_snapshots", OptionType.INTEGER, 5),
|
||||
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500),
|
||||
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_EXPERIENCE("synchronization.features.experience", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_POTION_EFFECTS("synchronization.features.potion_effects", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_ADVANCEMENTS("synchronization.features.advancements", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true);
|
||||
|
||||
/**
|
||||
* The path in the config.yml file to the value
|
||||
*/
|
||||
@NotNull
|
||||
public final String configPath;
|
||||
private Map.Entry<String, String> toEntry() {
|
||||
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName);
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link OptionType} of this option
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@NotNull
|
||||
public final OptionType optionType;
|
||||
|
||||
/**
|
||||
* The default value of this option if not set in config
|
||||
*/
|
||||
@Nullable
|
||||
private final Object defaultValue;
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType, @Nullable Object defaultValue) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string
|
||||
*/
|
||||
public String getStringValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getString(configPath, (String) defaultValue)
|
||||
: config.getString(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a boolean
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getBoolean(configPath, (Boolean) defaultValue)
|
||||
: config.getBoolean(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a double
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getDouble(configPath, (Double) defaultValue)
|
||||
: config.getDouble(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a float
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a float
|
||||
*/
|
||||
public float getFloatValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getFloat(configPath, (Float) defaultValue)
|
||||
: config.getFloat(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as an int
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as an int
|
||||
*/
|
||||
public int getIntValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getInt(configPath, (Integer) defaultValue)
|
||||
: config.getInt(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string {@link List}
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string {@link List}
|
||||
*/
|
||||
public List<String> getStringListValue(@NotNull YamlDocument config) {
|
||||
return config.getStringList(configPath, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the type of the object
|
||||
*/
|
||||
public enum OptionType {
|
||||
BOOLEAN,
|
||||
STRING,
|
||||
DOUBLE,
|
||||
FLOAT,
|
||||
INTEGER,
|
||||
STRING_LIST
|
||||
private static Map<String, String> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(TableName::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A mapped piece of advancement data
|
||||
*/
|
||||
public class AdvancementData {
|
||||
|
||||
/**
|
||||
* The advancement namespaced key
|
||||
*/
|
||||
@SerializedName("key")
|
||||
public String key;
|
||||
|
||||
/**
|
||||
* A map of completed advancement criteria to when it was completed
|
||||
*/
|
||||
@SerializedName("completed_criteria")
|
||||
public Map<String, Date> completedCriteria;
|
||||
|
||||
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = awardedCriteria;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected AdvancementData() {
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.xerial.snappy.Snappy;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CompressedDataAdapter extends JsonDataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
try {
|
||||
return Snappy.compress(super.toBytes(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to compress data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return super.fromBytes(Snappy.uncompress(data));
|
||||
} catch (IOException e) {
|
||||
throw new DataAdaptionException("Failed to decompress data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
368
common/src/main/java/net/william278/husksync/data/Data.java
Normal file
368
common/src/main/java/net/william278/husksync/data/Data.java
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A piece of data, held by a {@link DataHolder}
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public interface Data {
|
||||
|
||||
/**
|
||||
* Apply (set) this data container to the given {@link OnlineUser}
|
||||
*
|
||||
* @param user the user to apply this element to
|
||||
* @param plugin the plugin instance
|
||||
*/
|
||||
void apply(@NotNull UserDataHolder user, @NotNull HuskSync plugin);
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Inventories</li>
|
||||
* <li>Ender Chests</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Items extends Data {
|
||||
|
||||
@NotNull
|
||||
Stack[] getStack();
|
||||
|
||||
default int getSlotCount() {
|
||||
return getStack().length;
|
||||
}
|
||||
|
||||
record Stack(@NotNull String material, int amount, @Nullable String name,
|
||||
@Nullable List<String> lore, @NotNull List<String> enchantments) {
|
||||
|
||||
}
|
||||
|
||||
default boolean isEmpty() {
|
||||
return Arrays.stream(getStack()).allMatch(Objects::isNull) || getStack().length == 0;
|
||||
}
|
||||
|
||||
void clear();
|
||||
|
||||
void setContents(@NotNull Items contents);
|
||||
|
||||
/**
|
||||
* A data container holding data for inventories and selected hotbar slot
|
||||
*/
|
||||
interface Inventory extends Items {
|
||||
|
||||
int getHeldItemSlot();
|
||||
|
||||
void setHeldItemSlot(int heldItemSlot) throws IllegalArgumentException;
|
||||
|
||||
default Optional<Stack> getHelmet() {
|
||||
return Optional.ofNullable(getStack()[39]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getChestplate() {
|
||||
return Optional.ofNullable(getStack()[38]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getLeggings() {
|
||||
return Optional.ofNullable(getStack()[37]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getBoots() {
|
||||
return Optional.ofNullable(getStack()[36]);
|
||||
}
|
||||
|
||||
default Optional<Stack> getOffHand() {
|
||||
return Optional.ofNullable(getStack()[40]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for ender chests
|
||||
*/
|
||||
interface EnderChest extends Items {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for potion effects
|
||||
*/
|
||||
interface PotionEffects extends Data {
|
||||
|
||||
@NotNull
|
||||
List<Effect> getActiveEffects();
|
||||
|
||||
/**
|
||||
* Represents a potion effect
|
||||
*
|
||||
* @param type the type of potion effect
|
||||
* @param amplifier the amplifier of the potion effect
|
||||
* @param duration the duration of the potion effect
|
||||
* @param isAmbient whether the potion effect is ambient
|
||||
* @param showParticles whether the potion effect shows particles
|
||||
* @param hasIcon whether the potion effect displays a HUD icon
|
||||
*/
|
||||
record Effect(@SerializedName("type") @NotNull String type,
|
||||
@SerializedName("amplifier") int amplifier,
|
||||
@SerializedName("duration") int duration,
|
||||
@SerializedName("is_ambient") boolean isAmbient,
|
||||
@SerializedName("show_particles") boolean showParticles,
|
||||
@SerializedName("has_icon") boolean hasIcon) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for advancements
|
||||
*/
|
||||
interface Advancements extends Data {
|
||||
|
||||
@NotNull
|
||||
List<Advancement> getCompleted();
|
||||
|
||||
@NotNull
|
||||
default List<Advancement> getCompletedExcludingRecipes() {
|
||||
return getCompleted().stream()
|
||||
.filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe"))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
void setCompleted(@NotNull List<Advancement> completed);
|
||||
|
||||
class Advancement {
|
||||
@SerializedName("key")
|
||||
private String key;
|
||||
|
||||
@SerializedName("completed_criteria")
|
||||
private Map<String, Long> completedCriteria;
|
||||
|
||||
private Advancement(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
|
||||
this.key = key;
|
||||
this.completedCriteria = adaptDateMap(completedCriteria);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Advancement() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Advancement adapt(@NotNull String key, @NotNull Map<String, Date> completedCriteria) {
|
||||
return new Advancement(key, completedCriteria);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime()));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, Date> adaptLongMap(@NotNull Map<String, Long> dateMap) {
|
||||
return dateMap.entrySet().stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue())));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(@NotNull String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public Map<String, Date> getCompletedCriteria() {
|
||||
return adaptLongMap(completedCriteria);
|
||||
}
|
||||
|
||||
public void setCompletedCriteria(Map<String, Date> completedCriteria) {
|
||||
this.completedCriteria = adaptDateMap(completedCriteria);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for the player's location
|
||||
*/
|
||||
interface Location extends Data {
|
||||
double getX();
|
||||
|
||||
void setX(double x);
|
||||
|
||||
double getY();
|
||||
|
||||
void setY(double y);
|
||||
|
||||
double getZ();
|
||||
|
||||
void setZ(double z);
|
||||
|
||||
float getYaw();
|
||||
|
||||
void setYaw(float yaw);
|
||||
|
||||
float getPitch();
|
||||
|
||||
void setPitch(float pitch);
|
||||
|
||||
@NotNull
|
||||
World getWorld();
|
||||
|
||||
void setWorld(@NotNull World world);
|
||||
|
||||
record World(
|
||||
@SerializedName("name") @NotNull String name,
|
||||
@SerializedName("uuid") @NotNull UUID uuid,
|
||||
@SerializedName("environment") @NotNull String environment
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for statistics
|
||||
*/
|
||||
interface Statistics extends Data {
|
||||
@NotNull
|
||||
Map<String, Integer> getGenericStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getBlockStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getItemStatistics();
|
||||
|
||||
@NotNull
|
||||
Map<String, Map<String, Integer>> getEntityStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* Data container holding data for persistent data containers
|
||||
*/
|
||||
interface PersistentData extends Data {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Health</li>
|
||||
* <li>Max Health</li>
|
||||
* <li>Health Scale</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Health extends Data {
|
||||
double getHealth();
|
||||
|
||||
void setHealth(double health);
|
||||
|
||||
double getMaxHealth();
|
||||
|
||||
void setMaxHealth(double maxHealth);
|
||||
|
||||
double getHealthScale();
|
||||
|
||||
void setHealthScale(double healthScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
*
|
||||
* <li>Food Level</li>
|
||||
* <li>Saturation</li>
|
||||
* <li>Exhaustion</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Hunger extends Data {
|
||||
|
||||
int getFoodLevel();
|
||||
|
||||
void setFoodLevel(int foodLevel);
|
||||
|
||||
float getSaturation();
|
||||
|
||||
void setSaturation(float saturation);
|
||||
|
||||
float getExhaustion();
|
||||
|
||||
void setExhaustion(float exhaustion);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Total experience</li>
|
||||
* <li>Experience level</li>
|
||||
* <li>Experience progress</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface Experience extends Data {
|
||||
|
||||
int getTotalExperience();
|
||||
|
||||
void setTotalExperience(int totalExperience);
|
||||
|
||||
int getExpLevel();
|
||||
|
||||
void setExpLevel(int expLevel);
|
||||
|
||||
float getExpProgress();
|
||||
|
||||
void setExpProgress(float expProgress);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
* <li>Game mode</li>
|
||||
* <li>Allow flight</li>
|
||||
* <li>Is flying</li>
|
||||
* </ul>
|
||||
*/
|
||||
interface GameMode extends Data {
|
||||
|
||||
@NotNull
|
||||
String getGameMode();
|
||||
|
||||
void setGameMode(@NotNull String gameMode);
|
||||
|
||||
boolean getAllowFlight();
|
||||
|
||||
void setAllowFlight(boolean allowFlight);
|
||||
|
||||
boolean getIsFlying();
|
||||
|
||||
void setIsFlying(boolean isFlying);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* An adapter that adapts {@link UserData} to and from a portable byte array.
|
||||
*/
|
||||
public interface DataAdapter {
|
||||
|
||||
/**
|
||||
* Converts {@link UserData} to a byte array
|
||||
*
|
||||
* @param data The {@link UserData} to adapt
|
||||
* @return The byte array.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
byte[] toBytes(@NotNull UserData data) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Serializes {@link UserData} to a JSON string.
|
||||
*
|
||||
* @param data The {@link UserData} to serialize
|
||||
* @param pretty Whether to pretty print the JSON.
|
||||
* @return The output json string.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation.
|
||||
*/
|
||||
@NotNull
|
||||
String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException;
|
||||
|
||||
/**
|
||||
* Converts a byte array to {@link UserData}.
|
||||
*
|
||||
* @param data The byte array to adapt.
|
||||
* @return The {@link UserData}.
|
||||
* @throws DataAdaptionException If an error occurred during adaptation, such as if the byte array is invalid.
|
||||
*/
|
||||
@NotNull
|
||||
UserData fromBytes(final byte[] data) throws DataAdaptionException;
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
/**
|
||||
* Indicates an error occurred during {@link UserData} adaptation to and from (compressed) json.
|
||||
*/
|
||||
public class DataAdaptionException extends RuntimeException {
|
||||
protected DataAdaptionException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface DataHolder {
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getData();
|
||||
|
||||
default Optional<? extends Data> getData(@NotNull Identifier identifier) {
|
||||
return Optional.ofNullable(getData().get(identifier));
|
||||
}
|
||||
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getData().put(identifier, data);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Items.Inventory> getInventory() {
|
||||
return getData(Identifier.INVENTORY).map(Data.Items.Inventory.class::cast);
|
||||
}
|
||||
|
||||
default void setInventory(@NotNull Data.Items.Inventory inventory) {
|
||||
setData(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Items.EnderChest> getEnderChest() {
|
||||
return getData(Identifier.ENDER_CHEST).map(Data.Items.EnderChest.class::cast);
|
||||
}
|
||||
|
||||
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
setData(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PotionEffects> getPotionEffects() {
|
||||
return getData(Identifier.POTION_EFFECTS).map(Data.PotionEffects.class::cast);
|
||||
}
|
||||
|
||||
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
setData(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Advancements> getAdvancements() {
|
||||
return getData(Identifier.ADVANCEMENTS).map(Data.Advancements.class::cast);
|
||||
}
|
||||
|
||||
default void setAdvancements(@NotNull Data.Advancements advancements) {
|
||||
setData(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Location> getLocation() {
|
||||
return Optional.ofNullable((Data.Location) getData().get(Identifier.LOCATION));
|
||||
}
|
||||
|
||||
default void setLocation(@NotNull Data.Location location) {
|
||||
getData().put(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Statistics> getStatistics() {
|
||||
return Optional.ofNullable((Data.Statistics) getData().get(Identifier.STATISTICS));
|
||||
}
|
||||
|
||||
default void setStatistics(@NotNull Data.Statistics statistics) {
|
||||
getData().put(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Health> getHealth() {
|
||||
return Optional.ofNullable((Data.Health) getData().get(Identifier.HEALTH));
|
||||
}
|
||||
|
||||
default void setHealth(@NotNull Data.Health health) {
|
||||
getData().put(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Hunger> getHunger() {
|
||||
return Optional.ofNullable((Data.Hunger) getData().get(Identifier.HUNGER));
|
||||
}
|
||||
|
||||
default void setHunger(@NotNull Data.Hunger hunger) {
|
||||
getData().put(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||
}
|
||||
|
||||
default void setExperience(@NotNull Data.Experience experience) {
|
||||
getData().put(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.GameMode> getGameMode() {
|
||||
return Optional.ofNullable((Data.GameMode) getData().get(Identifier.GAME_MODE));
|
||||
}
|
||||
|
||||
default void setGameMode(@NotNull Data.GameMode gameMode) {
|
||||
getData().put(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.PersistentData> getPersistentData() {
|
||||
return Optional.ofNullable((Data.PersistentData) getData().get(Identifier.PERSISTENT_DATA));
|
||||
}
|
||||
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
getData().put(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.api.BaseHuskSyncAPI;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum DataSaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @see BaseHuskSyncAPI#saveUserData(OnlineUser)
|
||||
* @see BaseHuskSyncAPI#setUserData(User, UserData)
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved by an unknown cause.
|
||||
* </p>
|
||||
* This should not be used and is only used for error handling purposes.
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
UNKNOWN;
|
||||
|
||||
/**
|
||||
* Returns a {@link DataSaveCause} by name.
|
||||
*
|
||||
* @return the {@link DataSaveCause} or {@link #UNKNOWN} if the name is not valid.
|
||||
*/
|
||||
@NotNull
|
||||
public static DataSaveCause getCauseByName(@NotNull String name) {
|
||||
for (DataSaveCause cause : values()) {
|
||||
if (cause.name().equalsIgnoreCase(name)) {
|
||||
return cause;
|
||||
}
|
||||
}
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Indicates an error occurred during Base-64 serialization and deserialization of data.
|
||||
* </p>
|
||||
* For example, an exception deserializing {@link ItemData} item stack or {@link PotionEffectData} potion effect arrays
|
||||
*/
|
||||
public class DataSerializationException extends RuntimeException {
|
||||
protected DataSerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,840 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.Adaptable;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* A snapshot of a {@link DataHolder} at a given time.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public class DataSnapshot {
|
||||
|
||||
/*
|
||||
* Current version of the snapshot data format.
|
||||
* HuskSync v3.0 uses v4; HuskSync v2.0 uses v1-v3
|
||||
*/
|
||||
protected static final int CURRENT_FORMAT_VERSION = 4;
|
||||
|
||||
@SerializedName("id")
|
||||
protected UUID id;
|
||||
|
||||
@SerializedName("pinned")
|
||||
protected boolean pinned;
|
||||
|
||||
@SerializedName("timestamp")
|
||||
protected OffsetDateTime timestamp;
|
||||
|
||||
@SerializedName("save_cause")
|
||||
protected SaveCause saveCause;
|
||||
|
||||
@SerializedName("minecraft_version")
|
||||
protected String minecraftVersion;
|
||||
|
||||
@SerializedName("platform_type")
|
||||
protected String platformType;
|
||||
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion;
|
||||
|
||||
@SerializedName("data")
|
||||
protected Map<String, String> data;
|
||||
|
||||
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
this.id = id;
|
||||
this.pinned = pinned;
|
||||
this.timestamp = timestamp;
|
||||
this.saveCause = saveCause;
|
||||
this.data = data;
|
||||
this.minecraftVersion = minecraftVersion.toStringWithoutMetadata();
|
||||
this.platformType = platformType;
|
||||
this.formatVersion = formatVersion;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DataSnapshot() {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
|
||||
return new Builder(plugin);
|
||||
}
|
||||
|
||||
// Deserialize a DataSnapshot downloaded from the database (with an ID & Timestamp from the database)
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data, @Nullable UUID id,
|
||||
@Nullable OffsetDateTime timestamp) throws IllegalStateException {
|
||||
final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
|
||||
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " +
|
||||
"their user data (%s) is newer than the server's Minecraft version (%s)." +
|
||||
"Please ensure each server is running the same version of Minecraft.",
|
||||
snapshot.getMinecraftVersion(), plugin.getMinecraftVersion()));
|
||||
}
|
||||
if (snapshot.getFormatVersion() > CURRENT_FORMAT_VERSION) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the format version of " +
|
||||
"their user data (%s) is newer than the current format version (%s). " +
|
||||
"Please ensure each server is running the latest version of HuskSync.",
|
||||
snapshot.getFormatVersion(), CURRENT_FORMAT_VERSION));
|
||||
}
|
||||
if (snapshot.getFormatVersion() < CURRENT_FORMAT_VERSION) {
|
||||
if (plugin.getLegacyConverter().isPresent()) {
|
||||
return plugin.getLegacyConverter().get().convert(
|
||||
data,
|
||||
Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"),
|
||||
Objects.requireNonNull(timestamp, "Attempted legacy conversion with null timestamp!")
|
||||
);
|
||||
}
|
||||
throw new IllegalStateException(String.format(
|
||||
"No legacy converter to convert format version: %s", snapshot.getFormatVersion()
|
||||
));
|
||||
}
|
||||
if (!snapshot.getPlatformType().equalsIgnoreCase(plugin.getPlatformType())) {
|
||||
throw new IllegalStateException(String.format("Cannot set data for user because the platform type of " +
|
||||
"their user data (%s) is different to the server platform type (%s). " +
|
||||
"Please ensure each server is running the same platform type.",
|
||||
snapshot.getPlatformType(), plugin.getPlatformType()));
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
// Deserialize a DataSnapshot from a network message payload (without an ID)
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data) throws IllegalStateException {
|
||||
return deserialize(plugin, data, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the snapshot
|
||||
*
|
||||
* @return The snapshot ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the short display ID of the snapshot
|
||||
*
|
||||
* @return The short display ID
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public String getShortId() {
|
||||
return id.toString().substring(0, 8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the snapshot is pinned
|
||||
*
|
||||
* @return Whether the snapshot is pinned
|
||||
* @since 3.0
|
||||
*/
|
||||
public boolean isPinned() {
|
||||
return pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the snapshot is pinned
|
||||
*
|
||||
* @param pinned Whether the snapshot is pinned
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setPinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get why the snapshot was created
|
||||
*
|
||||
* @return The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public SaveCause getSaveCause() {
|
||||
return saveCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set why the snapshot was created
|
||||
*
|
||||
* @param saveCause The {@link SaveCause data save cause} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public void setSaveCause(SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get when the snapshot was created
|
||||
*
|
||||
* @return The {@link OffsetDateTime timestamp} of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public OffsetDateTime getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Minecraft version of the server when the Snapshot was created
|
||||
*
|
||||
* @return The Minecraft version of the server when the Snapshot was created
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromString(minecraftVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the platform type of the server when the Snapshot was created
|
||||
*
|
||||
* @return The platform type of the server when the Snapshot was created (e.g. {@code "bukkit"})
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public String getPlatformType() {
|
||||
return platformType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the format version of the snapshot (indicating the version of HuskSync that created it)
|
||||
*
|
||||
* @return The format version of the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* A packed {@link DataSnapshot} that has not been deserialized.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Packed extends DataSnapshot implements Adaptable {
|
||||
|
||||
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private Packed() {
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
|
||||
final Unpacked data = unpack(plugin);
|
||||
editor.accept(data);
|
||||
this.pinned = data.isPinned();
|
||||
this.saveCause = data.getSaveCause();
|
||||
this.data = data.serializeData(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of this snapshot at the current system timestamp with a new ID
|
||||
*
|
||||
* @return The copied snapshot (with a new ID, with a timestamp of the current system time)
|
||||
*/
|
||||
@NotNull
|
||||
public Packed copy() {
|
||||
return new Packed(
|
||||
UUID.randomUUID(), pinned, OffsetDateTime.now(), saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public byte[] asBytes(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toBytes(this);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public String asJson(@NotNull HuskSync plugin) throws DataAdapter.AdaptionException {
|
||||
return plugin.getDataAdapter().toJson(this);
|
||||
}
|
||||
|
||||
@ApiStatus.Internal
|
||||
public int getFileSize(@NotNull HuskSync plugin) {
|
||||
return asBytes(plugin).length;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked unpack(@NotNull HuskSync plugin) {
|
||||
return new Unpacked(
|
||||
id, pinned, timestamp, saveCause, data,
|
||||
getMinecraftVersion(), platformType, formatVersion, plugin
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An unpacked {@link DataSnapshot}.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
public static class Unpacked extends DataSnapshot implements DataHolder {
|
||||
|
||||
@Expose(serialize = false, deserialize = false)
|
||||
private final Map<Identifier, Data> deserialized;
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<String, String> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
|
||||
@NotNull HuskSync plugin) {
|
||||
super(id, pinned, timestamp, saveCause, data, minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = deserializeData(plugin);
|
||||
}
|
||||
|
||||
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
|
||||
@NotNull SaveCause saveCause, @NotNull Map<Identifier, Data> data,
|
||||
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
|
||||
super(id, pinned, timestamp, saveCause, Map.of(), minecraftVersion, platformType, formatVersion);
|
||||
this.deserialized = data;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
|
||||
return data.entrySet().stream()
|
||||
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry(
|
||||
id, plugin.getSerializers().get(id).deserialize(entry.getValue())
|
||||
)).orElse(null))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
private Map<String, String> serializeData(@NotNull HuskSync plugin) {
|
||||
return deserialized.entrySet().stream()
|
||||
.map((entry) -> Map.entry(entry.getKey().toString(),
|
||||
Objects.requireNonNull(
|
||||
plugin.getSerializers().get(entry.getKey()),
|
||||
String.format("No serializer found for %s", entry.getKey())
|
||||
).serialize(entry.getValue())))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data the snapshot is holding
|
||||
*
|
||||
* @return The data map
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Map<Identifier, Data> getData() {
|
||||
return deserialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pack the {@link DataSnapshot} into a {@link DataSnapshot.Packed packed} snapshot
|
||||
*
|
||||
* @param plugin The HuskSync plugin instance
|
||||
* @return The packed snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
public DataSnapshot.Packed pack(@NotNull HuskSync plugin) {
|
||||
return new DataSnapshot.Packed(
|
||||
id, pinned, timestamp, saveCause, serializeData(plugin),
|
||||
getMinecraftVersion(), platformType, formatVersion
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A builder for {@link DataSnapshot}s.
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static class Builder {
|
||||
|
||||
private final HuskSync plugin;
|
||||
private UUID id;
|
||||
private SaveCause saveCause;
|
||||
private boolean pinned;
|
||||
private OffsetDateTime timestamp;
|
||||
private final Map<Identifier, Data> data;
|
||||
|
||||
private Builder(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.pinned = false;
|
||||
this.data = new HashMap<>();
|
||||
this.timestamp = OffsetDateTime.now();
|
||||
this.id = UUID.randomUUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link UUID unique ID} of the snapshot
|
||||
*
|
||||
* @param id The {@link UUID} of the snapshot
|
||||
* @return The builder
|
||||
*/
|
||||
@NotNull
|
||||
public Builder id(@NotNull UUID id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the cause of the data save
|
||||
*
|
||||
* @param saveCause The cause of the data save
|
||||
* @return The builder
|
||||
* @apiNote If the {@link SaveCause data save cause} specified is configured to auto-pin, then the value of
|
||||
* {@link #pinned(boolean)} will be ignored
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder saveCause(@NotNull SaveCause saveCause) {
|
||||
this.saveCause = saveCause;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether the data should be pinned
|
||||
*
|
||||
* @param pinned Whether the data should be pinned
|
||||
* @return The builder
|
||||
* @apiNote If the {@link SaveCause data save cause} specified is configured to auto-pin, this will be ignored
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder pinned(boolean pinned) {
|
||||
this.pinned = pinned;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the timestamp of the snapshot.
|
||||
* By default, this is the current server time.
|
||||
* The timestamp passed to this method cannot be in the future.
|
||||
* <p>
|
||||
* Note that this will affect the rotation of data snapshots in the database if unpinned,
|
||||
* as well as the order snapshots appear in the list.
|
||||
*
|
||||
* @param timestamp The timestamp
|
||||
* @return The builder
|
||||
* @throws IllegalArgumentException if the timestamp is in the future
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder timestamp(@NotNull OffsetDateTime timestamp) {
|
||||
if (timestamp.isAfter(OffsetDateTime.now())) {
|
||||
throw new IllegalArgumentException("Data snapshots cannot have a timestamp set in the future");
|
||||
}
|
||||
this.timestamp = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the data for a given identifier
|
||||
*
|
||||
* @param identifier The identifier
|
||||
* @param data The data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder data(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
this.data.put(identifier, data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a map of data to the snapshot
|
||||
*
|
||||
* @param data The data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder data(@NotNull Map<Identifier, Data> data) {
|
||||
this.data.putAll(data);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory contents of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.INVENTORY, inventory)}
|
||||
* </p>
|
||||
*
|
||||
* @param inventory The inventory contents
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder inventory(@NotNull Data.Items.Inventory inventory) {
|
||||
return data(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Ender Chest contents of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ENDER_CHEST, inventory)}
|
||||
* </p>
|
||||
*
|
||||
* @param enderChest The Ender Chest contents
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder enderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
return data(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the potion effects of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.POTION_EFFECTS, potionEffects)}
|
||||
* </p>
|
||||
*
|
||||
* @param potionEffects The potion effects
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder potionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
return data(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the advancements of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ADVANCEMENTS, advancements)}
|
||||
* </p>
|
||||
*
|
||||
* @param advancements The advancements
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder advancements(@NotNull Data.Advancements advancements) {
|
||||
return data(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the location of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.LOCATION, location)}
|
||||
* </p>
|
||||
*
|
||||
* @param location The location
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder location(@NotNull Data.Location location) {
|
||||
return data(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the statistics of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.STATISTICS, statistics)}
|
||||
* </p>
|
||||
*
|
||||
* @param statistics The statistics
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder statistics(@NotNull Data.Statistics statistics) {
|
||||
return data(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the health of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.HEALTH, health)}
|
||||
* </p>
|
||||
*
|
||||
* @param health The health
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder health(@NotNull Data.Health health) {
|
||||
return data(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hunger of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.HUNGER, hunger)}
|
||||
* </p>
|
||||
*
|
||||
* @param hunger The hunger
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder hunger(@NotNull Data.Hunger hunger) {
|
||||
return data(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the experience of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.EXPERIENCE, experience)}
|
||||
* </p>
|
||||
*
|
||||
* @param experience The experience
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder experience(@NotNull Data.Experience experience) {
|
||||
return data(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the game mode of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.GAME_MODE, gameMode)}
|
||||
* </p>
|
||||
*
|
||||
* @param gameMode The game mode
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder gameMode(@NotNull Data.GameMode gameMode) {
|
||||
return data(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the persistent data container of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.PERSISTENT_DATA, persistentData)}
|
||||
* </p>
|
||||
*
|
||||
* @param persistentData The persistent data container data
|
||||
* @return The builder
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public Builder persistentData(@NotNull Data.PersistentData persistentData) {
|
||||
return data(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@link DataSnapshot}
|
||||
*
|
||||
* @return The {@link DataSnapshot.Unpacked snapshot}
|
||||
* @throws IllegalStateException If no save cause is specified
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Unpacked build() throws IllegalStateException {
|
||||
if (saveCause == null) {
|
||||
throw new IllegalStateException("Cannot build DataSnapshot without a save cause");
|
||||
}
|
||||
return new Unpacked(
|
||||
id,
|
||||
pinned || plugin.getSettings().doAutoPin(saveCause),
|
||||
timestamp,
|
||||
saveCause,
|
||||
data,
|
||||
plugin.getMinecraftVersion(),
|
||||
plugin.getPlatformType(),
|
||||
DataSnapshot.CURRENT_FORMAT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and pack the {@link DataSnapshot}
|
||||
*
|
||||
* @return The {@link DataSnapshot.Packed snapshot}
|
||||
* @throws IllegalStateException If no save cause is specified
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public DataSnapshot.Packed buildAndPack() throws IllegalStateException {
|
||||
return build().pack(plugin);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the cause of a player data save.
|
||||
*
|
||||
* @implNote This enum is saved in the database.
|
||||
* </p>
|
||||
* Cause names have a max length of 32 characters.
|
||||
*/
|
||||
public enum SaveCause {
|
||||
|
||||
/**
|
||||
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
DISCONNECT,
|
||||
/**
|
||||
* Indicates data saved when the world saved
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
SERVER_SHUTDOWN,
|
||||
/**
|
||||
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
INVENTORY_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
ENDERCHEST_COMMAND,
|
||||
/**
|
||||
* Indicates data was saved by restoring it from a previous version
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
BACKUP_RESTORE,
|
||||
/**
|
||||
* Indicates data was saved by an API call
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
API,
|
||||
/**
|
||||
* Indicates data was saved from being imported from MySQLPlayerDataBridge
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
MPDB_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
|
||||
*
|
||||
* @since 2.0
|
||||
*/
|
||||
LEGACY_MIGRATION,
|
||||
/**
|
||||
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
CONVERTED_FROM_V2;
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return Locales.truncate(name().toLowerCase(Locale.ENGLISH)
|
||||
.replaceAll("_", " "), 18);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the cause of a player having their data updated.
|
||||
*/
|
||||
public enum UpdateCause {
|
||||
/**
|
||||
* Indicates the data was updated by a synchronization process
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
|
||||
/**
|
||||
* Indicates the data was updated by a user joining the server
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
NEW_USER("user_registration_complete", null),
|
||||
/**
|
||||
* Indicates the data was updated by a data update process (management command, API, etc.)
|
||||
*
|
||||
* @since 3.0
|
||||
*/
|
||||
UPDATED("data_update_complete", "data_update_failed");
|
||||
|
||||
private final String completedLocale;
|
||||
private final String failureLocale;
|
||||
|
||||
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
|
||||
this.completedLocale = completedLocale;
|
||||
this.failureLocale = failureLocale;
|
||||
}
|
||||
|
||||
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
|
||||
if (completedLocale != null) {
|
||||
return plugin.getLocales().getLocale(completedLocale);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
|
||||
if (failureLocale != null) {
|
||||
return plugin.getLocales().getLocale(failureLocale);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.kyori.adventure.key.InvalidKeyException;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.intellij.lang.annotations.Subst;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Identifiers of different types of {@link Data}s
|
||||
*/
|
||||
public class Identifier {
|
||||
|
||||
public static Identifier INVENTORY = huskSync("inventory", true);
|
||||
public static Identifier ENDER_CHEST = huskSync("ender_chest", true);
|
||||
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true);
|
||||
public static Identifier ADVANCEMENTS = huskSync("advancements", true);
|
||||
public static Identifier LOCATION = huskSync("location", false);
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
|
||||
|
||||
private final Key key;
|
||||
private final boolean configDefault;
|
||||
|
||||
private Identifier(@NotNull Key key, boolean configDefault) {
|
||||
this.key = key;
|
||||
this.configDefault = configDefault;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a {@link Key}
|
||||
*
|
||||
* @param key the key
|
||||
* @return the identifier
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@NotNull Key key) {
|
||||
if (key.namespace().equals("husksync")) {
|
||||
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
|
||||
}
|
||||
return new Identifier(key, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an identifier from a namespace and value
|
||||
*
|
||||
* @param plugin the namespace
|
||||
* @param name the value
|
||||
* @return the identifier
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name) {
|
||||
return from(Key.key(plugin, name));
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Identifier huskSync(@Subst("null") @NotNull String name,
|
||||
boolean configDefault) throws InvalidKeyException {
|
||||
return new Identifier(Key.key("husksync", name), configDefault);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
private static Identifier parse(@NotNull String key) throws InvalidKeyException {
|
||||
return huskSync(key, true);
|
||||
}
|
||||
|
||||
public boolean isEnabledByDefault() {
|
||||
return configDefault;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Map.Entry<String, Boolean> getConfigEntry() {
|
||||
return Map.entry(getKeyValue(), configDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* <b>(Internal use only)</b> - Get a map of the default config entries for all HuskSync identifiers
|
||||
*
|
||||
* @return a map of all the config entries
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Boolean> getConfigMap() {
|
||||
return Map.ofEntries(Stream.of(
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, PERSISTENT_DATA
|
||||
)
|
||||
.map(Identifier::getConfigEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the namespace of the identifier
|
||||
*
|
||||
* @return the namespace
|
||||
*/
|
||||
@NotNull
|
||||
public String getKeyNamespace() {
|
||||
return key.namespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the identifier
|
||||
*
|
||||
* @return the value
|
||||
*/
|
||||
@NotNull
|
||||
public String getKeyValue() {
|
||||
return key.value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the identifier is a custom (non-HuskSync) identifier
|
||||
*
|
||||
* @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise
|
||||
*/
|
||||
public boolean isCustom() {
|
||||
return !getKeyNamespace().equals("husksync");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the identifier as a string (the key)
|
||||
*
|
||||
* @return the identifier as a string
|
||||
*/
|
||||
@NotNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key.asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*
|
||||
* @param obj the object to compare
|
||||
* @return {@code true} if the given object is an identifier with the same key as this identifier
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof Identifier other) {
|
||||
return key.equals(other.key);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores information about the contents of a player's inventory or Ender Chest.
|
||||
*/
|
||||
public class ItemData {
|
||||
|
||||
/**
|
||||
* A Base-64 string of platform-serialized items
|
||||
*/
|
||||
@SerializedName("serialized_items")
|
||||
public String serializedItems;
|
||||
|
||||
public ItemData(@NotNull final String serializedItems) {
|
||||
this.serializedItems = serializedItems;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected ItemData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class JsonDataAdapter implements DataAdapter {
|
||||
|
||||
@Override
|
||||
public byte[] toBytes(@NotNull UserData data) throws DataAdaptionException {
|
||||
return toJson(data, false).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String toJson(@NotNull UserData data, boolean pretty) throws DataAdaptionException {
|
||||
return (pretty ? new GsonBuilder().setPrettyPrinting() : new GsonBuilder()).create().toJson(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull UserData fromBytes(byte[] data) throws DataAdaptionException {
|
||||
try {
|
||||
return new GsonBuilder().create().fromJson(new String(data, StandardCharsets.UTF_8), UserData.class);
|
||||
} catch (JsonSyntaxException e) {
|
||||
throw new DataAdaptionException("Failed to parse JSON data", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores information about a player's location
|
||||
*/
|
||||
public class LocationData {
|
||||
|
||||
/**
|
||||
* Name of the world on the server
|
||||
*/
|
||||
@SerializedName("world_name")
|
||||
public String worldName;
|
||||
/**
|
||||
* Unique id of the world
|
||||
*/
|
||||
@SerializedName("world_uuid")
|
||||
public UUID worldUuid;
|
||||
/**
|
||||
* The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
|
||||
*/
|
||||
@SerializedName("world_environment")
|
||||
public String worldEnvironment;
|
||||
|
||||
/**
|
||||
* The x coordinate of the location
|
||||
*/
|
||||
@SerializedName("x")
|
||||
public double x;
|
||||
/**
|
||||
* The y coordinate of the location
|
||||
*/
|
||||
@SerializedName("y")
|
||||
public double y;
|
||||
/**
|
||||
* The z coordinate of the location
|
||||
*/
|
||||
@SerializedName("z")
|
||||
public double z;
|
||||
|
||||
/**
|
||||
* The location's facing yaw angle
|
||||
*/
|
||||
@SerializedName("yaw")
|
||||
public float yaw;
|
||||
/**
|
||||
* The location's facing pitch angle
|
||||
*/
|
||||
@SerializedName("pitch")
|
||||
public float pitch;
|
||||
|
||||
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
|
||||
@NotNull String worldEnvironment,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) {
|
||||
this.worldName = worldName;
|
||||
this.worldUuid = worldUuid;
|
||||
this.worldEnvironment = worldEnvironment;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected LocationData() {
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Store's a user's persistent data container, holding a map of plugin-set persistent values
|
||||
*/
|
||||
public class PersistentDataContainerData {
|
||||
|
||||
/**
|
||||
* Map of namespaced key strings to a byte array representing the persistent data
|
||||
*/
|
||||
@SerializedName("persistent_data_map")
|
||||
public Map<String, Byte[]> persistentDataMap;
|
||||
|
||||
public PersistentDataContainerData(@NotNull final Map<String, Byte[]> persistentDataMap) {
|
||||
this.persistentDataMap = persistentDataMap;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PersistentDataContainerData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores potion effect data
|
||||
*/
|
||||
public class PotionEffectData {
|
||||
|
||||
@SerializedName("serialized_potion_effects")
|
||||
public String serializedPotionEffects;
|
||||
|
||||
public PotionEffectData(@NotNull final String serializedPotionEffects) {
|
||||
this.serializedPotionEffects = serializedPotionEffects;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected PotionEffectData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface Serializer<T extends Data> {
|
||||
|
||||
T deserialize(@NotNull String serialized) throws DeserializationException;
|
||||
|
||||
@NotNull
|
||||
String serialize(@NotNull T element) throws SerializationException;
|
||||
|
||||
static final class DeserializationException extends IllegalStateException {
|
||||
DeserializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
static final class SerializationException extends IllegalStateException {
|
||||
SerializationException(@NotNull String message, @NotNull Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Stores information about a player's statistics
|
||||
*/
|
||||
public class StatisticsData {
|
||||
|
||||
/**
|
||||
* Map of untyped statistic names to their values
|
||||
*/
|
||||
@SerializedName("untyped_statistics")
|
||||
public Map<String, Integer> untypedStatistics;
|
||||
|
||||
/**
|
||||
* Map of block type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("block_statistics")
|
||||
public Map<String, Map<String, Integer>> blockStatistics;
|
||||
|
||||
/**
|
||||
* Map of item type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("item_statistics")
|
||||
public Map<String, Map<String, Integer>> itemStatistics;
|
||||
|
||||
/**
|
||||
* Map of entity type statistics to a map of entity types to values
|
||||
*/
|
||||
@SerializedName("entity_statistics")
|
||||
public Map<String, Map<String, Integer>> entityStatistics;
|
||||
|
||||
public StatisticsData(@NotNull Map<String, Integer> untypedStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> blockStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> itemStatistics,
|
||||
@NotNull Map<String, Map<String, Integer>> entityStatistics) {
|
||||
this.untypedStatistics = untypedStatistics;
|
||||
this.blockStatistics = blockStatistics;
|
||||
this.itemStatistics = itemStatistics;
|
||||
this.entityStatistics = entityStatistics;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatisticsData() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Stores status information about a player
|
||||
*/
|
||||
public class StatusData {
|
||||
|
||||
/**
|
||||
* The player's health points
|
||||
*/
|
||||
@SerializedName("health")
|
||||
public double health;
|
||||
|
||||
/**
|
||||
* The player's maximum health points
|
||||
*/
|
||||
@SerializedName("max_health")
|
||||
public double maxHealth;
|
||||
|
||||
/**
|
||||
* The player's health scaling factor
|
||||
*/
|
||||
@SerializedName("health_scale")
|
||||
public double healthScale;
|
||||
|
||||
/**
|
||||
* The player's hunger points
|
||||
*/
|
||||
@SerializedName("hunger")
|
||||
public int hunger;
|
||||
|
||||
/**
|
||||
* The player's saturation points
|
||||
*/
|
||||
@SerializedName("saturation")
|
||||
public float saturation;
|
||||
|
||||
/**
|
||||
* The player's saturation exhaustion points
|
||||
*/
|
||||
@SerializedName("saturation_exhaustion")
|
||||
public float saturationExhaustion;
|
||||
|
||||
/**
|
||||
* The player's currently selected item slot
|
||||
*/
|
||||
@SerializedName("selected_item_slot")
|
||||
public int selectedItemSlot;
|
||||
|
||||
/**
|
||||
* The player's total experience points<p>
|
||||
* (not to be confused with <i>experience level</i> - this is the "points" value shown on the death screen)
|
||||
*/
|
||||
@SerializedName("total_experience")
|
||||
public int totalExperience;
|
||||
|
||||
/**
|
||||
* The player's experience level (shown on the exp bar)
|
||||
*/
|
||||
@SerializedName("experience_level")
|
||||
public int expLevel;
|
||||
|
||||
/**
|
||||
* The player's progress to their next experience level
|
||||
*/
|
||||
@SerializedName("experience_progress")
|
||||
public float expProgress;
|
||||
|
||||
/**
|
||||
* The player's game mode string (one of "SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR")
|
||||
*/
|
||||
@SerializedName("game_mode")
|
||||
public String gameMode;
|
||||
|
||||
/**
|
||||
* If the player is currently flying
|
||||
*/
|
||||
@SerializedName("is_flying")
|
||||
public boolean isFlying;
|
||||
|
||||
public StatusData(final double health, final double maxHealth, final double healthScale,
|
||||
final int hunger, final float saturation, final float saturationExhaustion,
|
||||
final int selectedItemSlot, final int totalExperience, final int expLevel,
|
||||
final float expProgress, final String gameMode, final boolean isFlying) {
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedItemSlot = selectedItemSlot;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.isFlying = isFlying;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
protected StatusData() {
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.config.Settings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Flags for setting {@link StatusData}, indicating which elements should be synced
|
||||
*/
|
||||
public enum StatusDataFlag {
|
||||
|
||||
SET_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
|
||||
SET_MAX_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
|
||||
SET_HUNGER(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
|
||||
SET_EXPERIENCE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
|
||||
SET_GAME_MODE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
|
||||
SET_FLYING(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION),
|
||||
SET_SELECTED_ITEM_SLOT(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES);
|
||||
|
||||
private final Settings.ConfigOption configOption;
|
||||
|
||||
StatusDataFlag(@NotNull Settings.ConfigOption configOption) {
|
||||
this.configOption = configOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags
|
||||
*
|
||||
* @return all status data flags as a list
|
||||
*/
|
||||
@NotNull
|
||||
@SuppressWarnings("unused")
|
||||
public static List<StatusDataFlag> getAll() {
|
||||
return Arrays.stream(StatusDataFlag.values()).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all status data flags that are enabled for setting as per the {@link Settings}
|
||||
*
|
||||
* @param settings the settings to use for determining which flags are enabled
|
||||
* @return all status data flags that are enabled for setting
|
||||
*/
|
||||
@NotNull
|
||||
public static List<StatusDataFlag> getFromSettings(@NotNull Settings settings) {
|
||||
return Arrays.stream(StatusDataFlag.values()).filter(
|
||||
flag -> settings.getBooleanValue(flag.configOption)).toList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/***
|
||||
* Stores data about a user
|
||||
*/
|
||||
public class UserData {
|
||||
|
||||
/**
|
||||
* Indicates the version of the {@link UserData} format being used.
|
||||
* </p>
|
||||
* This value is to be incremented whenever the format changes.
|
||||
*/
|
||||
public static final int CURRENT_FORMAT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
*/
|
||||
@SerializedName("status")
|
||||
protected StatusData statusData;
|
||||
|
||||
/**
|
||||
* Stores the user's inventory contents
|
||||
*/
|
||||
@SerializedName("inventory")
|
||||
protected ItemData inventoryData;
|
||||
|
||||
/**
|
||||
* Stores the user's ender chest contents
|
||||
*/
|
||||
@SerializedName("ender_chest")
|
||||
protected ItemData enderChestData;
|
||||
|
||||
/**
|
||||
* Store's the user's potion effects
|
||||
*/
|
||||
@SerializedName("potion_effects")
|
||||
protected PotionEffectData potionEffectData;
|
||||
|
||||
/**
|
||||
* Stores the set of this user's advancements
|
||||
*/
|
||||
@SerializedName("advancements")
|
||||
protected List<AdvancementData> advancementData;
|
||||
|
||||
/**
|
||||
* Stores the user's set of statistics
|
||||
*/
|
||||
@SerializedName("statistics")
|
||||
protected StatisticsData statisticData;
|
||||
|
||||
/**
|
||||
* Store's the user's world location and coordinates
|
||||
*/
|
||||
@SerializedName("location")
|
||||
protected LocationData locationData;
|
||||
|
||||
/**
|
||||
* Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
|
||||
*/
|
||||
@SerializedName("persistent_data_container")
|
||||
protected PersistentDataContainerData persistentDataContainerData;
|
||||
|
||||
/**
|
||||
* Stores the version of Minecraft this data was generated in
|
||||
*/
|
||||
@SerializedName("minecraft_version")
|
||||
protected String minecraftVersion;
|
||||
|
||||
/**
|
||||
* Stores the version of the data format being used
|
||||
*/
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion;
|
||||
|
||||
public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData,
|
||||
@NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData,
|
||||
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
|
||||
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData,
|
||||
@NotNull String minecraftVersion) {
|
||||
this.statusData = statusData;
|
||||
this.inventoryData = inventoryData;
|
||||
this.enderChestData = enderChestData;
|
||||
this.potionEffectData = potionEffectData;
|
||||
this.advancementData = advancementData;
|
||||
this.statisticData = statisticData;
|
||||
this.locationData = locationData;
|
||||
this.persistentDataContainerData = persistentDataContainerData;
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
this.formatVersion = CURRENT_FORMAT_VERSION;
|
||||
}
|
||||
|
||||
// Empty constructor to facilitate json serialization
|
||||
@SuppressWarnings("unused")
|
||||
protected UserData() {
|
||||
}
|
||||
|
||||
public StatusData getStatusData() {
|
||||
return statusData;
|
||||
}
|
||||
|
||||
public ItemData getInventoryData() {
|
||||
return inventoryData;
|
||||
}
|
||||
|
||||
public ItemData getEnderChestData() {
|
||||
return enderChestData;
|
||||
}
|
||||
|
||||
public PotionEffectData getPotionEffectsData() {
|
||||
return potionEffectData;
|
||||
}
|
||||
|
||||
public List<AdvancementData> getAdvancementData() {
|
||||
return advancementData;
|
||||
}
|
||||
|
||||
public StatisticsData getStatisticsData() {
|
||||
return statisticData;
|
||||
}
|
||||
|
||||
public LocationData getLocationData() {
|
||||
return locationData;
|
||||
}
|
||||
|
||||
public PersistentDataContainerData getPersistentDataContainerData() {
|
||||
return persistentDataContainerData;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getMinecraftVersion() {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.desertwell.util.ThrowingConsumer;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* A holder of data in the form of {@link Data}s, which can be synced
|
||||
*/
|
||||
public interface UserDataHolder extends DataHolder {
|
||||
|
||||
/**
|
||||
* Get the data that is enabled for syncing in the config
|
||||
*
|
||||
* @return the data that is enabled for syncing
|
||||
* @since 3.0
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
default Map<Identifier, Data> getData() {
|
||||
return getPlugin().getRegisteredDataTypes().stream()
|
||||
.filter(type -> type.isCustom() || getPlugin().getSettings().isSyncFeatureEnabled(type))
|
||||
.map(id -> Map.entry(id, getData(id)))
|
||||
.filter(data -> data.getValue().isPresent())
|
||||
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the data for the given {@link Identifier} to the holder.
|
||||
* <p>
|
||||
* This will be performed synchronously on the main server thread; it will not happen instantly.
|
||||
*
|
||||
* @param identifier the {@link Identifier} to set the data for
|
||||
* @param data the {@link Data} to set
|
||||
* @since 3.0
|
||||
*/
|
||||
@Override
|
||||
default void setData(@NotNull Identifier identifier, @NotNull Data data) {
|
||||
getPlugin().runSync(() -> data.apply(this, getPlugin()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a serialized data snapshot of this data owner
|
||||
*
|
||||
* @param saveCause the cause of the snapshot
|
||||
* @return the snapshot
|
||||
* @since 3.0
|
||||
*/
|
||||
@NotNull
|
||||
default DataSnapshot.Packed createSnapshot(@NotNull DataSnapshot.SaveCause saveCause) {
|
||||
return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize and apply a data snapshot to this data owner
|
||||
* <p>
|
||||
* This method will deserialize the data on the current thread, then synchronously apply it on
|
||||
* the main server thread.
|
||||
* </p>
|
||||
* The {@code runAfter} callback function will be run after the snapshot has been applied.
|
||||
*
|
||||
* @param snapshot the snapshot to apply
|
||||
* @param runAfter a consumer accepting a boolean value, indicating if the data was successfully applied,
|
||||
* which will be run after the snapshot has been applied
|
||||
* @since 3.0
|
||||
*/
|
||||
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<Boolean> runAfter) {
|
||||
final HuskSync plugin = getPlugin();
|
||||
|
||||
// Unpack the snapshot
|
||||
final DataSnapshot.Unpacked unpacked;
|
||||
try {
|
||||
unpacked = snapshot.unpack(plugin);
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, String.format("Failed to unpack data snapshot for %s", getUsername()), e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Synchronously attempt to apply the snapshot
|
||||
plugin.runSync(() -> {
|
||||
try {
|
||||
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
|
||||
final Identifier identifier = entry.getKey();
|
||||
if (plugin.getSettings().isSyncFeatureEnabled(identifier)) {
|
||||
if (identifier.isCustom()) {
|
||||
getCustomDataStore().put(identifier, entry.getValue());
|
||||
}
|
||||
entry.getValue().apply(this, plugin);
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);
|
||||
plugin.runAsync(() -> runAfter.accept(false));
|
||||
return;
|
||||
}
|
||||
plugin.runAsync(() -> runAfter.accept(true));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setInventory(@NotNull Data.Items.Inventory inventory) {
|
||||
this.setData(Identifier.INVENTORY, inventory);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setEnderChest(@NotNull Data.Items.EnderChest enderChest) {
|
||||
this.setData(Identifier.ENDER_CHEST, enderChest);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPotionEffects(@NotNull Data.PotionEffects potionEffects) {
|
||||
this.setData(Identifier.POTION_EFFECTS, potionEffects);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setAdvancements(@NotNull Data.Advancements advancements) {
|
||||
this.setData(Identifier.ADVANCEMENTS, advancements);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setLocation(@NotNull Data.Location location) {
|
||||
this.setData(Identifier.LOCATION, location);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setStatistics(@NotNull Data.Statistics statistics) {
|
||||
this.setData(Identifier.STATISTICS, statistics);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setHealth(@NotNull Data.Health health) {
|
||||
this.setData(Identifier.HEALTH, health);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setHunger(@NotNull Data.Hunger hunger) {
|
||||
this.setData(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setExperience(@NotNull Data.Experience experience) {
|
||||
this.setData(Identifier.EXPERIENCE, experience);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setGameMode(@NotNull Data.GameMode gameMode) {
|
||||
this.setData(Identifier.GAME_MODE, gameMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
default void setPersistentData(@NotNull Data.PersistentData persistentData) {
|
||||
this.setData(Identifier.PERSISTENT_DATA, persistentData);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
String getUsername();
|
||||
|
||||
@NotNull
|
||||
Map<Identifier, Data> getCustomDataStore();
|
||||
|
||||
@NotNull
|
||||
@ApiStatus.Internal
|
||||
HuskSync getPlugin();
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a uniquely versioned and timestamped snapshot of a user's data, including why it was saved.
|
||||
*
|
||||
* @param versionUUID The unique identifier for this user data version
|
||||
* @param versionTimestamp An epoch milliseconds timestamp of when this data was created
|
||||
* @param userData The {@link UserData} that has been versioned
|
||||
* @param cause The {@link DataSaveCause} that caused this data to be saved
|
||||
*/
|
||||
public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
|
||||
@NotNull DataSaveCause cause, boolean pinned,
|
||||
@NotNull UserData userData) implements Comparable<UserDataSnapshot> {
|
||||
|
||||
/**
|
||||
* Version {@link UserData} into a {@link UserDataSnapshot}, assigning it a random {@link UUID} and the current timestamp {@link Date}
|
||||
* </p>
|
||||
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
|
||||
*
|
||||
* @param userData The {@link UserData} to version
|
||||
* @return A new {@link UserDataSnapshot}
|
||||
* @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.<p>
|
||||
* Database implementations should instead use their own UUID generation functions.
|
||||
*/
|
||||
public static UserDataSnapshot create(@NotNull UserData userData) {
|
||||
return new UserDataSnapshot(UUID.randomUUID(), new Date(),
|
||||
DataSaveCause.API, false, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UserData by creation timestamp
|
||||
*
|
||||
* @param other the other UserData to be compared
|
||||
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NotNull UserDataSnapshot other) {
|
||||
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +1,40 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.data.DataSnapshot.SaveCause;
|
||||
import net.william278.husksync.data.UserDataHolder;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* An abstract representation of the plugin database, storing player data.
|
||||
@@ -25,78 +43,10 @@ import java.util.concurrent.CompletableFuture;
|
||||
*/
|
||||
public abstract class Database {
|
||||
|
||||
/**
|
||||
* Name of the table that stores player information
|
||||
*/
|
||||
protected final String playerTableName;
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* Name of the table that stores data
|
||||
*/
|
||||
protected final String dataTableName;
|
||||
|
||||
/**
|
||||
* The maximum number of user records to store in the database at once per user
|
||||
*/
|
||||
protected final int maxUserDataRecords;
|
||||
|
||||
/**
|
||||
* {@link DataAdapter} implementation used for adapting {@link UserData} to and from JSON
|
||||
*/
|
||||
private final DataAdapter dataAdapter;
|
||||
|
||||
/**
|
||||
* Returns the {@link DataAdapter} used to adapt {@link UserData} to and from JSON
|
||||
*
|
||||
* @return instance of the {@link DataAdapter} implementation
|
||||
*/
|
||||
protected DataAdapter getDataAdapter() {
|
||||
return dataAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link EventCannon} implementation used for firing events
|
||||
*/
|
||||
private final EventCannon eventCannon;
|
||||
|
||||
/**
|
||||
* Returns the {@link EventCannon} used to fire events
|
||||
*
|
||||
* @return instance of the {@link EventCannon} implementation
|
||||
*/
|
||||
protected EventCannon getEventCannon() {
|
||||
return eventCannon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logger instance used for database error logging
|
||||
*/
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* Returns the {@link Logger} used to log database errors
|
||||
*
|
||||
* @return the {@link Logger} instance
|
||||
*/
|
||||
protected Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ResourceReader} used to read internal resource files by name
|
||||
*/
|
||||
private final ResourceReader resourceReader;
|
||||
|
||||
protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
|
||||
@NotNull ResourceReader resourceReader, @NotNull DataAdapter dataAdapter,
|
||||
@NotNull EventCannon eventCannon, @NotNull Logger logger) {
|
||||
this.playerTableName = playerTableName;
|
||||
this.dataTableName = dataTableName;
|
||||
this.maxUserDataRecords = maxUserDataRecords;
|
||||
this.resourceReader = resourceReader;
|
||||
this.dataAdapter = dataAdapter;
|
||||
this.eventCannon = eventCannon;
|
||||
this.logger = logger;
|
||||
protected Database(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,139 +57,244 @@ public abstract class Database {
|
||||
* @throws IOException if the resource could not be read
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NotNull
|
||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||
return formatStatementTables(new String(resourceReader.getResource(schemaFileName)
|
||||
return formatStatementTables(new String(Objects.requireNonNull(plugin.getResource(schemaFileName))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all table name placeholder strings in a SQL statement
|
||||
* Format all table name placeholder strings in an SQL statement
|
||||
*
|
||||
* @param sql the SQL statement with un-formatted table name placeholders
|
||||
* @param sql the SQL statement with unformatted table name placeholders
|
||||
* @return the formatted statement, with table placeholders replaced with the correct names
|
||||
*/
|
||||
@NotNull
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
return sql.replaceAll("%users_table%", playerTableName)
|
||||
.replaceAll("%user_data_table%", dataTableName);
|
||||
return sql.replaceAll("%users_table%", plugin.getSettings().getTableName(Settings.TableName.USERS))
|
||||
.replaceAll("%user_data_table%", plugin.getSettings().getTableName(Settings.TableName.USER_DATA));
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @return A future returning boolean - if the connection could be established.
|
||||
* @throws IllegalStateException if the database could not be initialized
|
||||
*/
|
||||
public abstract boolean initialize();
|
||||
@Blocking
|
||||
public abstract void initialize() throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> ensureUser(@NotNull User user);
|
||||
@Blocking
|
||||
public abstract void ensureUser(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid);
|
||||
@Blocking
|
||||
public abstract Optional<User> getUser(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
* @return An optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUserByName(@NotNull String username);
|
||||
@Blocking
|
||||
public abstract Optional<User> getUserByName(@NotNull String username);
|
||||
|
||||
|
||||
/**
|
||||
* Get the current uniquely versioned user data for a given user, if it exists.
|
||||
*
|
||||
* @param user the user to get data for
|
||||
* @return an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get all {@link UserDataSnapshot} entries for a user from the database.
|
||||
* Get the latest data snapshot for a user.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return A future returning a list of a user's {@link UserDataSnapshot} entries
|
||||
* @return an optional containing the {@link DataSnapshot}, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user);
|
||||
@Blocking
|
||||
public abstract Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||
* Get all {@link DataSnapshot} entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return The list of a user's {@link DataSnapshot} entries
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
public abstract List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Gets a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to get
|
||||
* @return A future returning an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to get
|
||||
* @return An optional containing the {@link DataSnapshot}, if it exists
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
public abstract Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @return A future returning void when complete
|
||||
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||
*/
|
||||
protected abstract CompletableFuture<Void> rotateUserData(@NotNull User user);
|
||||
@Blocking
|
||||
protected abstract void rotateSnapshots(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Deletes a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||
* Deletes a specific {@link DataSnapshot} entry for a user from the database, by its UUID.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to delete
|
||||
* @return A future returning void when complete
|
||||
* @param versionUuid The UUID of the {@link DataSnapshot} entry to delete
|
||||
*/
|
||||
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid);
|
||||
|
||||
/**
|
||||
* Save user data to the database<p>
|
||||
* Save user data to the database
|
||||
* </p>
|
||||
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @return A future returning void when complete
|
||||
* @see UserDataSnapshot#create(UserData)
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
* The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||
* @see UserDataHolder#createSnapshot(SaveCause)
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
|
||||
@Blocking
|
||||
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
if (snapshot.getSaveCause() != SaveCause.SERVER_SHUTDOWN) {
|
||||
plugin.fireEvent(
|
||||
plugin.getDataSaveEvent(user, snapshot),
|
||||
(event) -> this.addAndRotateSnapshot(user, snapshot)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.addAndRotateSnapshot(user, snapshot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
|
||||
* <b>Internal</b> - Save user data to the database. This will:
|
||||
* <ol>
|
||||
* <li>Delete their most recent snapshot, if it was created before the backup frequency time</li>
|
||||
* <li>Create the snapshot</li>
|
||||
* <li>Rotate snapshot backups</li>
|
||||
* </ol>
|
||||
*
|
||||
* @param user The user to pin the data for
|
||||
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to pin
|
||||
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||
* @see UserDataSnapshot#pinned()
|
||||
* @param user The user to add data for
|
||||
* @param snapshot The {@link DataSnapshot} to set.
|
||||
*/
|
||||
public abstract CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
|
||||
final int backupFrequency = plugin.getSettings().getBackupFrequency();
|
||||
if (!snapshot.isPinned() && backupFrequency > 0) {
|
||||
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));
|
||||
}
|
||||
this.createSnapshot(user, snapshot);
|
||||
this.rotateSnapshots(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
|
||||
* Deletes the most recent data snapshot by the given {@link User user}
|
||||
* The snapshot must have been created after {@link OffsetDateTime time} and NOT be pinned
|
||||
* Facilities the backup frequency feature, reducing redundant snapshots from being saved longer than needed
|
||||
*
|
||||
* @param user The user to delete a snapshot for
|
||||
* @param within The time to delete a snapshot after
|
||||
*/
|
||||
@Blocking
|
||||
protected abstract void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within);
|
||||
|
||||
/**
|
||||
* <b>Internal</b> - Create user data in the database
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param data The {@link DataSnapshot} to set.
|
||||
*/
|
||||
@Blocking
|
||||
protected abstract void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data);
|
||||
|
||||
/**
|
||||
* Update a saved {@link DataSnapshot} by given version UUID
|
||||
*
|
||||
* @param user The user whose data snapshot
|
||||
* @param snapshot The {@link DataSnapshot} to update
|
||||
*/
|
||||
@Blocking
|
||||
public abstract void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot);
|
||||
|
||||
/**
|
||||
* Unpin a saved {@link DataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
|
||||
*
|
||||
* @param user The user to unpin the data for
|
||||
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to unpin
|
||||
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||
* @see UserDataSnapshot#pinned()
|
||||
* @param versionUuid The UUID of the user's {@link DataSnapshot} entry to unpin
|
||||
* @see DataSnapshot#isPinned()
|
||||
*/
|
||||
public abstract CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||
@Blocking
|
||||
public final void unpinSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
this.getSnapshot(user, versionUuid).ifPresent(data -> {
|
||||
data.edit(plugin, (snapshot) -> snapshot.setPinned(false));
|
||||
this.updateSnapshot(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link UserData} entries from the database.
|
||||
* <b>This should never be used</b>, except when preparing tables for migration.
|
||||
* Pin a saved {@link DataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
|
||||
*
|
||||
* @return A future returning void when complete
|
||||
* @see Migrator#start()
|
||||
* @param user The user to pin the data for
|
||||
* @param versionUuid The UUID of the user's {@link DataSnapshot} entry to pin
|
||||
*/
|
||||
public abstract CompletableFuture<Void> wipeDatabase();
|
||||
@Blocking
|
||||
public final void pinSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
this.getSnapshot(user, versionUuid).ifPresent(data -> {
|
||||
data.edit(plugin, (snapshot) -> snapshot.setPinned(true));
|
||||
this.updateSnapshot(user, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipes <b>all</b> {@link User} entries from the database.
|
||||
* <b>This should only be used when preparing tables for a data migration.</b>
|
||||
*/
|
||||
@Blocking
|
||||
public abstract void wipeDatabase();
|
||||
|
||||
/**
|
||||
* Close the database connection
|
||||
*/
|
||||
public abstract void close();
|
||||
public abstract void terminate();
|
||||
|
||||
/**
|
||||
* Identifies types of databases
|
||||
*/
|
||||
public enum Type {
|
||||
MYSQL("MySQL", "mysql"),
|
||||
MARIADB("MariaDB", "mariadb");
|
||||
|
||||
private final String displayName;
|
||||
private final String protocol;
|
||||
|
||||
Type(@NotNull String displayName, @NotNull String protocol) {
|
||||
this.displayName = displayName;
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,73 +1,51 @@
|
||||
/*
|
||||
* This file is part of HuskSync, licensed under the Apache License 2.0.
|
||||
*
|
||||
* Copyright (c) William278 <will27528@gmail.com>
|
||||
* Copyright (c) contributors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.event.DataSaveEvent;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.data.DataSnapshot;
|
||||
import net.william278.husksync.user.User;
|
||||
import org.jetbrains.annotations.Blocking;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.util.Date;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MySqlDatabase extends Database {
|
||||
|
||||
/**
|
||||
* MySQL server hostname
|
||||
*/
|
||||
private final String mySqlHost;
|
||||
|
||||
/**
|
||||
* MySQL server port
|
||||
*/
|
||||
private final int mySqlPort;
|
||||
|
||||
/**
|
||||
* Database to use on the MySQL server
|
||||
*/
|
||||
private final String mySqlDatabaseName;
|
||||
private final String mySqlUsername;
|
||||
private final String mySqlPassword;
|
||||
private final String mySqlConnectionParameters;
|
||||
|
||||
private final int hikariMaximumPoolSize;
|
||||
private final int hikariMinimumIdle;
|
||||
private final int hikariMaximumLifetime;
|
||||
private final int hikariKeepAliveTime;
|
||||
private final int hikariConnectionTimeOut;
|
||||
|
||||
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
|
||||
private final String flavor;
|
||||
private final String driverClass;
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
/**
|
||||
* The Hikari data source - a pool of database connections that can be fetched on-demand
|
||||
*/
|
||||
private HikariDataSource connectionPool;
|
||||
|
||||
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger,
|
||||
@NotNull DataAdapter dataAdapter, @NotNull EventCannon eventCannon) {
|
||||
super(settings.getStringValue(Settings.ConfigOption.DATABASE_USERS_TABLE_NAME),
|
||||
settings.getStringValue(Settings.ConfigOption.DATABASE_USER_DATA_TABLE_NAME),
|
||||
Math.max(1, Math.min(20, settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS))),
|
||||
resourceReader, dataAdapter, eventCannon, logger);
|
||||
this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
this.mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
this.mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
this.mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
this.mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS);
|
||||
this.hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE);
|
||||
this.hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE);
|
||||
this.hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME);
|
||||
this.hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE);
|
||||
this.hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT);
|
||||
public MySqlDatabase(@NotNull HuskSync plugin) {
|
||||
super(plugin);
|
||||
this.flavor = plugin.getSettings().getDatabaseType().getProtocol();
|
||||
this.driverClass = plugin.getSettings().getDatabaseType() == Type.MARIADB
|
||||
? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,356 +54,370 @@ public class MySqlDatabase extends Database {
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
@Blocking
|
||||
@NotNull
|
||||
private Connection getConnection() throws SQLException {
|
||||
return connectionPool.getConnection();
|
||||
if (dataSource == null) {
|
||||
throw new IllegalStateException("The database has not been initialized");
|
||||
}
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean initialize() {
|
||||
try {
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters;
|
||||
connectionPool = new HikariDataSource();
|
||||
connectionPool.setJdbcUrl(jdbcUrl);
|
||||
public void initialize() throws IllegalStateException {
|
||||
// Initialize the Hikari pooled connection
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setDriverClassName(driverClass);
|
||||
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
|
||||
flavor,
|
||||
plugin.getSettings().getMySqlHost(),
|
||||
plugin.getSettings().getMySqlPort(),
|
||||
plugin.getSettings().getMySqlDatabase(),
|
||||
plugin.getSettings().getMySqlConnectionParameters()
|
||||
));
|
||||
|
||||
// Authenticate
|
||||
connectionPool.setUsername(mySqlUsername);
|
||||
connectionPool.setPassword(mySqlPassword);
|
||||
// Authenticate with the database
|
||||
dataSource.setUsername(plugin.getSettings().getMySqlUsername());
|
||||
dataSource.setPassword(plugin.getSettings().getMySqlPassword());
|
||||
|
||||
// Set various additional parameters
|
||||
connectionPool.setMaximumPoolSize(hikariMaximumPoolSize);
|
||||
connectionPool.setMinimumIdle(hikariMinimumIdle);
|
||||
connectionPool.setMaxLifetime(hikariMaximumLifetime);
|
||||
connectionPool.setKeepaliveTime(hikariKeepAliveTime);
|
||||
connectionPool.setConnectionTimeout(hikariConnectionTimeOut);
|
||||
connectionPool.setPoolName(DATA_POOL_NAME);
|
||||
// Set connection pool options
|
||||
dataSource.setMaximumPoolSize(plugin.getSettings().getMySqlConnectionPoolSize());
|
||||
dataSource.setMinimumIdle(plugin.getSettings().getMySqlConnectionPoolIdle());
|
||||
dataSource.setMaxLifetime(plugin.getSettings().getMySqlConnectionPoolLifetime());
|
||||
dataSource.setKeepaliveTime(plugin.getSettings().getMySqlConnectionPoolKeepAlive());
|
||||
dataSource.setConnectionTimeout(plugin.getSettings().getMySqlConnectionPoolTimeout());
|
||||
dataSource.setPoolName(DATA_POOL_NAME);
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = connectionPool.getConnection()) {
|
||||
// Load database schema CREATE statements from schema file
|
||||
final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql");
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
// Set additional connection pool properties
|
||||
final Properties properties = new Properties();
|
||||
properties.putAll(
|
||||
Map.of("cachePrepStmts", "true",
|
||||
"prepStmtCacheSize", "250",
|
||||
"prepStmtCacheSqlLimit", "2048",
|
||||
"useServerPrepStmts", "true",
|
||||
"useLocalSessionState", "true",
|
||||
"useLocalTransactionState", "true"
|
||||
));
|
||||
properties.putAll(
|
||||
Map.of(
|
||||
"rewriteBatchedStatements", "true",
|
||||
"cacheResultSetMetadata", "true",
|
||||
"cacheServerConfiguration", "true",
|
||||
"elideSetAutoCommits", "true",
|
||||
"maintainTimeStats", "false")
|
||||
);
|
||||
dataSource.setDataSourceProperties(properties);
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
final String[] databaseSchema = getSchemaStatements(String.format("database/%s_schema.sql", flavor));
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new IllegalStateException("Failed to create database tables. Please ensure you are running MySQL v8.0+ " +
|
||||
"and that your connecting user account has privileges to create tables.", e);
|
||||
}
|
||||
} catch (SQLException | IOException e) {
|
||||
throw new IllegalStateException("Failed to establish a connection to the MySQL database. " +
|
||||
"Please check the supplied database credentials in the config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public void ensureUser(@NotNull User user) {
|
||||
getUser(user.getUuid()).ifPresentOrElse(
|
||||
existingUser -> {
|
||||
if (!existingUser.getUsername().equals(user.getUsername())) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%users_table%`
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, user.getUsername());
|
||||
statement.setString(2, existingUser.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
plugin.log(Level.INFO, "Updated " + user.getUsername() + "'s name in the database (" + existingUser.getUsername() + " -> " + user.getUsername() + ")");
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%users_table%` (`uuid`,`username`)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, user.getUsername());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (SQLException | IOException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage());
|
||||
);
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUser(@NotNull UUID uuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, uuid.toString());
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
getLogger().log(Level.SEVERE, "An unhandled exception occurred during database setup!", e);
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<User> getUserByName(@NotNull String username) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `username`=?"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final UUID versionUuid = UUID.fromString(resultSet.getString("version_uuid"));
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
@NotNull
|
||||
public List<DataSnapshot.Packed> getAllSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> retrievedData = new ArrayList<>();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UUID versionUuid = UUID.fromString(resultSet.getString("version_uuid"));
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final OffsetDateTime timestamp = OffsetDateTime.ofInstant(
|
||||
resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId()
|
||||
);
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
protected void rotateSnapshots(@NotNull User user) {
|
||||
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
|
||||
if (unpinnedUserData.size() > plugin.getSettings().getMaxUserDataSnapshots()) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - plugin.getSettings().getMaxUserDataSnapshots()))))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Void> ensureUser(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> getUser(user.uuid).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(existingUser -> {
|
||||
if (!existingUser.username.equals(user.username)) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%users_table%`
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, user.username);
|
||||
statement.setString(2, existingUser.uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
getLogger().log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")");
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%users_table%` (`uuid`,`username`)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, user.username);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, uuid.toString());
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
protected void rotateLatestSnapshot(@NotNull User user, @NotNull OffsetDateTime within) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `timestamp`>? AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setTimestamp(2, Timestamp.from(within.toInstant()));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to delete a user's data from the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUserByName(@NotNull String username) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%users_table%`
|
||||
WHERE `username`=?"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
protected void createSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%user_data_table%`
|
||||
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`pinned`,`data`)
|
||||
VALUES (?,?,?,?,?,?);"""))) {
|
||||
statement.setString(1, user.getUuid().toString());
|
||||
statement.setString(2, data.getId().toString());
|
||||
statement.setTimestamp(3, Timestamp.from(data.getTimestamp().toInstant()));
|
||||
statement.setString(4, data.getSaveCause().name());
|
||||
statement.setBoolean(5, data.isPinned());
|
||||
statement.setBlob(6, new ByteArrayInputStream(data.asBytes(plugin)));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (SQLException | DataAdapter.AdaptionException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
getDataAdapter().fromBytes(dataByteArray)));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
public void updateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed data) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `save_cause`=?,`pinned`=?,`data`=?
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, data.getSaveCause().name());
|
||||
statement.setBoolean(2, data.isPinned());
|
||||
statement.setBlob(3, new ByteArrayInputStream(data.asBytes(plugin)));
|
||||
statement.setString(4, user.getUuid().toString());
|
||||
statement.setString(5, data.getId().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final List<UserDataSnapshot> retrievedData = new ArrayList<>();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
final UserDataSnapshot data = new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
getDataAdapter().fromBytes(dataByteArray));
|
||||
retrievedData.add(data);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
public void wipeDatabase() {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;"));
|
||||
}
|
||||
return retrievedData;
|
||||
});
|
||||
} catch (SQLException e) {
|
||||
plugin.log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||
FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final Blob blob = resultSet.getBlob("data");
|
||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||
blob.free();
|
||||
return Optional.of(new UserDataSnapshot(
|
||||
UUID.fromString(resultSet.getString("version_uuid")),
|
||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||
resultSet.getBoolean("pinned"),
|
||||
getDataAdapter().fromBytes(dataByteArray)));
|
||||
}
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch specific user data by UUID from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Void> rotateUserData(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream()
|
||||
.filter(dataSnapshot -> !dataSnapshot.pinned()).toList();
|
||||
if (unpinnedUserData.size() > maxUserDataRecords) {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
AND `pinned` IS FALSE
|
||||
ORDER BY `timestamp` ASC
|
||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||
Integer.toString(unpinnedUserData.size() - maxUserDataRecords))))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%user_data_table%`
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
return statement.executeUpdate() > 0;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to delete specific user data from the database", e);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull DataSaveCause saveCause) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
final DataSaveEvent dataSaveEvent = (DataSaveEvent) getEventCannon().fireDataSaveEvent(user,
|
||||
userData, saveCause).join();
|
||||
if (!dataSaveEvent.isCancelled()) {
|
||||
final UserData finalData = dataSaveEvent.getUserData();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%user_data_table%`
|
||||
(`player_uuid`,`version_uuid`,`timestamp`,`save_cause`,`data`)
|
||||
VALUES (?,UUID(),NOW(),?,?);"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, saveCause.name());
|
||||
statement.setBlob(3, new ByteArrayInputStream(
|
||||
getDataAdapter().toBytes(finalData)));
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException | DataAdaptionException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}
|
||||
}).thenRun(() -> rotateUserData(user).join());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `pinned`=TRUE
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%user_data_table%`
|
||||
SET `pinned`=FALSE
|
||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, versionUuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to unpin user data in the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> wipeDatabase() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
statement.executeUpdate(formatStatementTables("DELETE FROM `%user_data_table%`;"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to wipe the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (connectionPool != null) {
|
||||
if (!connectionPool.isClosed()) {
|
||||
connectionPool.close();
|
||||
public void terminate() {
|
||||
if (dataSource != null) {
|
||||
if (!dataSource.isClosed()) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.AdvancementData;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Provides methods for displaying and editing user data
|
||||
*/
|
||||
public class DataEditor {
|
||||
|
||||
/**
|
||||
* Map of currently open inventory and ender chest data editors
|
||||
*/
|
||||
@NotNull
|
||||
protected final HashMap<UUID, ItemEditorMenu> openInventoryMenus;
|
||||
|
||||
private final Locales locales;
|
||||
|
||||
public DataEditor(@NotNull Locales locales) {
|
||||
this.openInventoryMenus = new HashMap<>();
|
||||
this.locales = locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an inventory or ender chest editor menu
|
||||
*
|
||||
* @param user The online user to open the editor for
|
||||
* @param itemEditorMenu The {@link ItemEditorMenu} to open
|
||||
* @see ItemEditorMenu#createInventoryMenu(ItemData, User, OnlineUser, Locales, boolean)
|
||||
* @see ItemEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser, Locales, boolean)
|
||||
*/
|
||||
public CompletableFuture<ItemData> openItemEditorMenu(@NotNull OnlineUser user,
|
||||
@NotNull ItemEditorMenu itemEditorMenu) {
|
||||
this.openInventoryMenus.put(user.uuid, itemEditorMenu);
|
||||
return itemEditorMenu.showInventory(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close an inventory or ender chest editor menu
|
||||
*
|
||||
* @param user The online user to close the editor for
|
||||
* @param itemData the {@link ItemData} contained within the menu at the time of closing
|
||||
*/
|
||||
public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull ItemData itemData) {
|
||||
if (this.openInventoryMenus.containsKey(user.uuid)) {
|
||||
this.openInventoryMenus.get(user.uuid).closeInventory(itemData);
|
||||
}
|
||||
this.openInventoryMenus.remove(user.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether edits to the inventory or ender chest menu are allowed
|
||||
*
|
||||
* @param user The online user with an inventory open to check
|
||||
* @return {@code true} if edits to the inventory or ender chest menu are allowed; {@code false} otherwise, including if they don't have an inventory open
|
||||
*/
|
||||
public boolean cancelMenuEdit(@NotNull OnlineUser user) {
|
||||
if (this.openInventoryMenus.containsKey(user.uuid)) {
|
||||
return !this.openInventoryMenus.get(user.uuid).canEdit;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a chat menu detailing information about {@link UserDataSnapshot}
|
||||
*
|
||||
* @param user The online user to display the message to
|
||||
* @param userData The {@link UserDataSnapshot} to display information about
|
||||
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||
*/
|
||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData,
|
||||
@NotNull User dataOwner) {
|
||||
locales.getLocale("data_manager_title",
|
||||
userData.versionUUID().toString().split("-")[0],
|
||||
userData.versionUUID().toString(),
|
||||
dataOwner.username,
|
||||
dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (userData.pinned()) {
|
||||
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause",
|
||||
userData.cause().name().toLowerCase().replaceAll("_", " "))
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) userData.userData().getStatusData().health),
|
||||
Integer.toString((int) userData.userData().getStatusData().maxHealth),
|
||||
Integer.toString(userData.userData().getStatusData().hunger),
|
||||
Integer.toString(userData.userData().getStatusData().expLevel),
|
||||
userData.userData().getStatusData().gameMode.toLowerCase())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(userData.userData().getAdvancementData().size()),
|
||||
generateAdvancementPreview(userData.userData().getAdvancementData()),
|
||||
String.format("%.2f", (((userData.userData().getStatisticsData().untypedStatistics.getOrDefault(
|
||||
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
|
||||
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
|
||||
locales.getLocale("data_manager_item_buttons",
|
||||
dataOwner.username, userData.versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
locales.getLocale("data_manager_management_buttons",
|
||||
dataOwner.username, userData.versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementsToPreview.size(); i++) {
|
||||
joiner.add(advancementsToPreview.get(i).key);
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE;
|
||||
if (remainingAdvancements > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…"));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a chat list detailing a player's saved list of {@link UserDataSnapshot}
|
||||
*
|
||||
* @param user The online user to display the message to
|
||||
* @param userDataList The list of {@link UserDataSnapshot} to display
|
||||
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||
*/
|
||||
public void displayDataList(@NotNull OnlineUser user, @NotNull List<UserDataSnapshot> userDataList,
|
||||
@NotNull User dataOwner) {
|
||||
locales.getLocale("data_list_title",
|
||||
dataOwner.username, dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
|
||||
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
|
||||
final UserDataSnapshot userData = userDataList.get(i);
|
||||
locales.getLocale("data_list_item",
|
||||
numberedIcons[i],
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userData.versionTimestamp()),
|
||||
userData.versionUUID().toString().split("-")[0],
|
||||
userData.versionUUID().toString(),
|
||||
userData.cause().name().toLowerCase().replaceAll("_", " "),
|
||||
dataOwner.username,
|
||||
userData.pinned() ? "※" : " ")
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user has an inventory editor menu open
|
||||
*
|
||||
* @param user {@link OnlineUser} to check
|
||||
* @return {@code true} if the user has an inventory editor open; {@code false} otherwise
|
||||
*/
|
||||
public Optional<ItemEditorMenu> getEditingInventoryData(@NotNull OnlineUser user) {
|
||||
return this.openInventoryMenus.containsKey(user.uuid) ? Optional.of(this.openInventoryMenus.get(user.uuid))
|
||||
: Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ItemEditorMenu {
|
||||
|
||||
public final ItemData itemData;
|
||||
public final ItemEditorMenuType itemEditorMenuType;
|
||||
public final MineDown menuTitle;
|
||||
public final boolean canEdit;
|
||||
|
||||
private CompletableFuture<ItemData> inventoryDataCompletableFuture;
|
||||
|
||||
private ItemEditorMenu(@NotNull ItemData itemData, ItemEditorMenuType itemEditorMenuType,
|
||||
@NotNull MineDown menuTitle, boolean canEdit) {
|
||||
this.itemData = itemData;
|
||||
this.menuTitle = menuTitle;
|
||||
this.itemEditorMenuType = itemEditorMenuType;
|
||||
this.canEdit = canEdit;
|
||||
}
|
||||
|
||||
public CompletableFuture<ItemData> showInventory(@NotNull OnlineUser user) {
|
||||
inventoryDataCompletableFuture = new CompletableFuture<>();
|
||||
user.showMenu(this);
|
||||
return inventoryDataCompletableFuture;
|
||||
}
|
||||
|
||||
public void closeInventory(@NotNull ItemData itemData) {
|
||||
inventoryDataCompletableFuture.complete(itemData);
|
||||
}
|
||||
|
||||
public static ItemEditorMenu createInventoryMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
|
||||
@NotNull OnlineUser viewer, @NotNull Locales locales,
|
||||
boolean canEdit) {
|
||||
return new ItemEditorMenu(itemData, ItemEditorMenuType.INVENTORY_VIEWER,
|
||||
locales.getLocale(ItemEditorMenuType.INVENTORY_VIEWER.localeKey, dataOwner.username).orElse(new MineDown("")),
|
||||
viewer.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node) && canEdit);
|
||||
}
|
||||
|
||||
public static ItemEditorMenu createEnderChestMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
|
||||
@NotNull OnlineUser viewer, @NotNull Locales locales,
|
||||
boolean canEdit) {
|
||||
return new ItemEditorMenu(itemData, ItemEditorMenuType.ENDER_CHEST_VIEWER,
|
||||
locales.getLocale(ItemEditorMenuType.ENDER_CHEST_VIEWER.localeKey, dataOwner.username).orElse(new MineDown("")),
|
||||
viewer.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node) && canEdit);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public enum ItemEditorMenuType {
|
||||
INVENTORY_VIEWER(45, "inventory_viewer_menu_title"),
|
||||
ENDER_CHEST_VIEWER(27, "ender_chest_viewer_menu_title");
|
||||
|
||||
public final int slotCount;
|
||||
final String localeKey;
|
||||
|
||||
ItemEditorMenuType(int slotCount, @NotNull String localeKey) {
|
||||
this.slotCount = slotCount;
|
||||
this.localeKey = localeKey;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user