diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..d7936f8d --- /dev/null +++ b/.github/workflows/ci.yml @@ -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' \ No newline at end of file diff --git a/.github/workflows/java_ci.yml b/.github/workflows/java_ci.yml deleted file mode 100644 index ac303370..00000000 --- a/.github/workflows/java_ci.yml +++ /dev/null @@ -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 diff --git a/.github/workflows/pr_tests.yml b/.github/workflows/pr_tests.yml new file mode 100644 index 00000000..2f74d405 --- /dev/null +++ b/.github/workflows/pr_tests.yml @@ -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 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f1c02964 --- /dev/null +++ b/.github/workflows/release.yml @@ -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' \ No newline at end of file diff --git a/.github/workflows/update_docs.yml b/.github/workflows/update_docs.yml new file mode 100644 index 00000000..fd188eb0 --- /dev/null +++ b/.github/workflows/update_docs.yml @@ -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]' \ No newline at end of file diff --git a/HEADER b/HEADER new file mode 100644 index 00000000..d3aa5a91 --- /dev/null +++ b/HEADER @@ -0,0 +1,10 @@ +This file is part of HuskSync by William278. Do not redistribute! + + Copyright (c) William278 + All rights reserved. + + This source code is provided as reference to licensed individuals that have purchased the HuskTowns + 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. \ No newline at end of file diff --git a/README.md b/README.md index 46b076ac..317472f4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@

HuskSync - - + + diff --git a/build.gradle b/build.gradle index 9939fe77..25932962 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,19 @@ plugins { id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'org.ajoberstar.grgit' version '5.0.0' - id 'java' + 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()}" +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 'snappy_version', snappy_version.toString() @@ -20,6 +24,7 @@ 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' @@ -48,6 +53,12 @@ allprojects { useJUnitPlatform() } + license { + header = rootProject.file('HEADER') + include '**/*.java' + newLine = true + } + processResources { filter ReplaceTokens as Class, beginToken: '${', endToken: '}', tokens: rootProject.ext.properties @@ -58,6 +69,10 @@ subprojects { version rootProject.version archivesBaseName = "${rootProject.name}-${project.name.capitalize()}" + jar { + from '../LICENSE' + } + if (['bukkit', 'plugin'].contains(project.name)) { shadowJar { destinationDirectory.set(file("$rootDir/target")) @@ -79,6 +94,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' diff --git a/bukkit/src/main/resources/plugin.yml b/bukkit/src/main/resources/plugin.yml index a284c626..100061b7 100644 --- a/bukkit/src/main/resources/plugin.yml +++ b/bukkit/src/main/resources/plugin.yml @@ -1,13 +1,13 @@ -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: - 'redis.clients:jedis:${jedis_version}' - 'com.mysql:mysql-connector-j:${mysql_driver_version}' @@ -16,14 +16,14 @@ libraries: commands: husksync: - usage: '/husksync ' + usage: '/ ' description: 'Manage the HuskSync plugin' userdata: - usage: '/userdata [version_uuid]' + usage: '/ [version_uuid]' description: 'View, manage & restore player userdata' inventory: - usage: '/inventory [version_uuid]' + usage: '/ [version_uuid]' description: 'View & edit a player''s inventory' enderchest: - usage: '/enderchest [version_uuid]' + usage: '/ [version_uuid]' description: 'View & edit a player''s Ender Chest' \ No newline at end of file diff --git a/docs/API-Events.md b/docs/API-Events.md new file mode 100644 index 00000000..b6605dad --- /dev/null +++ b/docs/API-Events.md @@ -0,0 +1,12 @@ +HuskSync provides three API events your plugin can listen to when certain parts of the data synchronisation process are performed. These events deal in HuskSync class types, so you may want to familiarize yourself with the [API basics](API) first. Two of the events can be cancelled (thus aborting the synchronisation process at certain stages) and some of the events expose methods letting you affect their outcome (such as modifying the data that is saved during the process). + +Consult the Javadocs for more information -- and don't forget to register your listener when listening for these event calls. Please note that carrying out expensive blocking operations during these events is strongly discouraged as this may affect plugin performance. + +## List of API Events +| Bukkit Event class | Cancellable | Description | +|---------------------------|:-----------:|---------------------------------------------------------------------------------------------| +| `BukkitDataSaveEvent` | ✅ | Called when player data snapshot is created, saved and cached due to a DataSaveCause | +| `BukkitPreSync` | ✅ | Called before a player has their data updated from the cache or database, just after login | +| `BukkitSyncCompleteEvent` | ❌ | Called once a player has completed their data synchronisation on login successfully† | + +†This can also fire when a user's data is updated while the player is logged in; i.e. when an admin rolls back the user, updates their inventory or Ender Chest through the respective commands, or when an API call is made forcing the user to have their data updated. \ No newline at end of file diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 00000000..c84b70df --- /dev/null +++ b/docs/API.md @@ -0,0 +1,77 @@ +[![HuskSync banner](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/banner.png)](https://github.com/WiIIiam278/HuskSync) +# HuskSync API v2 +![](https://jitpack.io/v/WiIIiam278/HuskSync.svg) + +The HuskSync API provides methods for retrieving and updating user data, as well as a number of events for tracking when user data is synced and saved. + +The API is distributed via [JitPack](https://jitpack.io/#net.william278/HuskSync). Please note that the HuskSync v1 API is not compatible. +(Some) javadocs are also available to view on Jitpack [here](https://javadoc.jitpack.io/net/william278/HuskSync/latest/javadoc/). + +## Table of contents +1. Adding the API to your project +2. Adding HuskSync as a dependency +3. Next steps + +## API Introduction +### 1.1 Setup with Maven +

+Maven setup information + +- Add the repository to your `pom.xml` as per below. +```xml + + + jitpack.io + https://jitpack.io + + +``` +- Add the dependency to your `pom.xml` as per below. Replace `version` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square) +```xml + + net.william278 + HuskSync + version + provided + +``` +
+ +### 1.2 Setup with Gradle +
+Gradle setup information + +- Add the dependency like so to your `build.gradle`: +```groovy +allprojects { + repositories { + maven { url 'https://jitpack.io' } + } +} +``` +- Add the dependency as per below. Replace `version` with the latest version of HuskSync (without the v): ![Latest version](https://img.shields.io/github/v/tag/WiIIiam278/HuskSync?color=%23282828&label=%20&style=flat-square) + +```groovy +dependencies { + compileOnly 'net.william278:HuskSync:version' +} +``` +
+ +### 2. Adding HuskSync as a dependency +- Add HuskSync to your `softdepend` (if you want to optionally use HuskSync) or `depend` (if your plugin relies on HuskSync) section in `plugin.yml` of your project. + +```yaml +name: MyPlugin +version: 1.0 +main: net.william278.myplugin.MyPlugin +author: William278 +description: 'A plugin that hooks with the HuskSync API!' +softdepend: # Or, use 'depend' here + - HuskSync +``` + +### 3. Next steps +Now that you've got everything ready, you can start doing stuff with the HuskSync API! +- [[UserData API]] — Get data snapshots and update current user data +- [[API Events]] — Listen to, cancel and modify the result of data synchronization events \ No newline at end of file diff --git a/docs/Commands.md b/docs/Commands.md new file mode 100644 index 00000000..c5ab087b --- /dev/null +++ b/docs/Commands.md @@ -0,0 +1,17 @@ +This page contains a table of HuskSync commands and their required permission nodes. + +| Command | Description | Permission | +|-----------------------------------------------|--------------------------------------|--------------------------------------| +| `/husksync` | Use `/husksync` subcommands | `husksync.command.husksync` | +| `/husksync info` | View plugin information | `husksync.command.husksync info` | +| `/husksync reload` | Reload config & message files | `husksync.command.husksync.reload` | +| `/husksync update` | Check if an update is available | `husksync.command.husksync.update` | +| `/husksync migrate [args]` | Migrate user data | _Console-only_ | +| `/userdata view [version_uuid]` | View a snapshot of user data | `husksync.command.userdata` | +| `/userdata restore ` | Restore a snapshot of user data | `husksync.command.userdata.manage` | +| `/userdata delete ` | Delete a snapshot of user data | `husksync.command.userdata.manage` | +| `/userdata pin ` | Pin a snapshot of user data | `husksync.command.userdata.manage` | +| `/inventory [version_uuid]` | View a user's inventory contents | `husksync.command.inventory`† | +| `/enderchest [version_uuid]` | View a user's ender chest contents | `husksync.command.enderchest`†| + +† The respective `husksync.command.inventory.edit` and `husksync.command.enderchest.edit` permission node is required to be able to edit a user's inventory/ender chest using the interface. \ No newline at end of file diff --git a/docs/Config-File.md b/docs/Config-File.md new file mode 100644 index 00000000..9e57b9ea --- /dev/null +++ b/docs/Config-File.md @@ -0,0 +1,78 @@ +This page contains the configuration file reference for HuskSync. The config file is located in `/plugins/HuskSync/config.yml` + +## Example config +
+config.yml + +```yaml +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ HuskSync Config ┃ +# ┃ Developed by William278 ┃ +# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +# ┣╸ Information: https://william278.net/project/husksync +# ┗╸ Documentation: https://william278.net/docs/husksync +language: en-gb +check_for_updates: true +cluster_id: '' +debug_logging: false +database: + credentials: + # Database connection settings + host: localhost + port: 3306 + database: HuskSync + username: root + password: pa55w0rd + parameters: ?autoReconnect=true&useSSL=false + connection_pool: + # MySQL connection pool properties + maximum_pool_size: 10 + minimum_idle: 10 + maximum_lifetime: 1800000 + keepalive_time: 0 + connection_timeout: 5000 + table_names: + users: husksync_users + user_data: husksync_user_data +redis: + credentials: + # Redis connection settings + host: localhost + port: 6379 + password: '' + use_ssl: false +synchronization: + # Synchronization settings + max_user_data_snapshots: 5 + save_on_world_save: true + save_on_death: false + save_empty_drops_on_death: true + compress_data: true + notification_display_slot: ACTION_BAR + synchronise_dead_players_changing_server: true + network_latency_milliseconds: 500 + features: + health: true + statistics: true + persistent_data_container: false + hunger: true + ender_chests: true + advancements: true + location: false + game_mode: true + potion_effects: true + locked_maps: false + inventories: true + max_health: true + experience: true + blacklisted_commands_while_locked: [] + event_priorities: + join_listener: LOWEST + quit_listener: LOWEST + death_listener: NORMAL +``` + +
+ +## Messages files +You can customize the plugin locales, too, by editing your `messages-xx-xx.yml` file. This file is formatted using [MineDown syntax](https://github.com/Phoenix616/MineDown). For more information, see [[Translations]]. \ No newline at end of file diff --git a/docs/Data-Rotation.md b/docs/Data-Rotation.md new file mode 100644 index 00000000..274ea5be --- /dev/null +++ b/docs/Data-Rotation.md @@ -0,0 +1,45 @@ +HuskSync provides options for backing up and automatically rotating user data. That way, if something goes wrong, you can restore a user from a previous snapshot of user data. + +## Snapshots +HuskSync creates what is known as "Snapshots" of a user's data whenever it saves data. + +Each user data snapshot has: +- a unique ID +- a timestamp (when it was created) +- a save cause (why it was created) +- a flag to indicate if the snapshot has been pinned (preventing it from being rotated) + +By default, HuskSync will store 5 user data snapshots about a user (including their latest snapshot). After that, when a new user snapshot is set, the oldest snapshot will automatically be deleted. You can change the number of snapshots to keep by changing the `max_user_data_snapshots` setting in the `config.yml` file (minimum 1). + +Pinned user data snapshots are exempt from being rotated and can only be deleted manually in-game. + +## Viewing user data +To view a list of a user's snapshots, use `/userdata list `. Their most recent snapshots will be listed from the database, from newest to oldest. You can click the buttons to navigate through their pages. + +[![Data snapshot list](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/data-snapshot-list.png)](#) + +Snapshots marked with a star after the number have been pinned. Hover over it to view more information. + +You can then click on the items in the list in chat to view an overview of each snapshot. Alternatively, to view a user's most recent data snapshot, use `/userdata view `. + +[![Data snapshot viewer](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/data-snapshot-viewer.png)](#) + +## Managing user data +You can use the "Manage" buttons to manage user data. These buttons will only appear if you have the userdata command manage permission. (See [[Commands]]) +- Click on "Delete" to remove the data +- Click on "Restore" to restore the user data. If the user is online, their items and stats will be updated, otherwise they will be set to this data upon next login. +- Click on "Pin" to pin the user data. An indicator will appear in the data viewer and list marking the snapshot as being pinned. + +## Save causes +Data save causes, marked with a 🚩 flag, indicate what caused the data to be saved. + +- **disconnect**: Indicates data saved when a player disconnected from the server (either to change servers, or to log off) +- **world save**: Indicates data saved when the world saved. This can be turned off in `config.yml` by setting `save_on_world_save` to false under `synchronization`. +- **server shutdown**: Indicates data saved when the server shut down +- **inventory command**: Indicates data was saved by editing inventory contents via the `/inventory` command +- **enderchest command**: Indicates data was saved by editing Ender Chest contents via the `/enderchest` command +- **backup restore**: Indicates data was saved by restoring it from a previous version +- **api**: Indicates data was saved by an [[API]] call +- **mpdb migration**: Indicates data was saved from being imported from MySQLPlayerDataBridge (See [[MPDB Migration]]) +- **legacy migration**: Indicates data was saved from being imported from a legacy version (v1.x - See [[Legacy Migration]]) +- **unknown**: Indicates data was saved by an unknown cause. \ No newline at end of file diff --git a/docs/Dumping-UserData.md b/docs/Dumping-UserData.md new file mode 100644 index 00000000..a322cebc --- /dev/null +++ b/docs/Dumping-UserData.md @@ -0,0 +1,324 @@ +It's possible to dump user data snapshots to `json` objects as of HuskSync v2.1, either to a file or to a web paste service (`mc.lo.gs`). This can be performed through the `/userdata dump` command. + +This can be useful in debugging synchronization problems or for manually inspecting data. + +## How-to guide +1. Grant yourself the special `husksync.command.userdata.dump` permission node. This is not set by default, even for operators. +2. Use the `/userdata list ` command to view a list of user data entries for a user. +3. Click on one of the user data entries for your chosen user. The data snapshot preview menu should appear, along with two new buttons at the bottom. + +[![Data dumping buttons](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/data-dumping.png)](#) + +### Dumping to a file +After clicking the "File Dump..." button (equivalent to `/userdata dump file`), a dump of this user data entry will be output in `~/plugins/HuskSync/dumps/`. + +The name of the generated .json file will match the following format: `___.json` + +
+Example output file: William278_2022-10-12_21-46-37_disconnect_f7719f5c.json + +```json +{ + "status": { + "health": 20.0, + "max_health": 20.0, + "health_scale": 0.0, + "hunger": 20, + "saturation": 0.0, + "saturation_exhaustion": 0.24399996, + "selected_item_slot": 1, + "total_experience": 0, + "experience_level": 0, + "experience_progress": 0.0, + "game_mode": "CREATIVE", + "is_flying": true + }, + "inventory": { + "serialized_items": "rO0ABXcEAAAAKXBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBwcHBzcgAXamF2YS51dGlsLkxp\r\nbmtlZEhhc2hNYXA0wE5cEGzA+wIAAVoAC2FjY2Vzc09yZGVyeHIAEWphdmEudXRpbC5IYXNoTWFw\r\nBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAADHcIAAAAEAAAAAJ0\r\nAAF2c3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcu\r\nTnVtYmVyhqyVHQuU4IsCAAB4cAAADDB0AAR0eXBldAASTEVBVEhFUl9DSEVTVFBMQVRFeABwc3EA\r\nfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+AAd0AAdCRURST0NLdAAGYW1v\r\ndW50c3EAfgAEAAAAQHgAcHBwcHBwcA\u003d\u003d\r\n" + }, + "ender_chest": { + "serialized_items": "rO0ABXcEAAAAG3NyABdqYXZhLnV0aWwuTGlua2VkSGFzaE1hcDTATlwQbMD7AgABWgALYWNjZXNz\r\nT3JkZXJ4cgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJl\r\nc2hvbGR4cD9AAAAAAAAMdwgAAAAQAAAAA3QAAXZzcgARamF2YS5sYW5nLkludGVnZXIS4qCk94GH\r\nOAIAAUkABXZhbHVleHIAEGphdmEubGFuZy5OdW1iZXKGrJUdC5TgiwIAAHhwAAAMMHQABHR5cGV0\r\nAA1TUFJVQ0VfUExBTktTdAAGYW1vdW50c3EAfgAEAAAAQHgAc3EAfgAAP0AAAAAAAAx3CAAAABAA\r\nAAADcQB+AANzcQB+AAQAAAwwcQB+AAd0AAdCRURST0NLcQB+AAlxAH4ACngAc3EAfgAAP0AAAAAA\r\nAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+AAdxAH4ADXEAfgAJcQB+AAp4AHNxAH4AAD9A\r\nAAAAAAAMdwgAAAAQAAAAA3EAfgADc3EAfgAEAAAMMHEAfgAHcQB+AA1xAH4ACXEAfgAKeABzcQB+\r\nAAA/QAAAAAAADHcIAAAAEAAAAANxAH4AA3NxAH4ABAAADDBxAH4AB3EAfgANcQB+AAlxAH4ACngA\r\nc3EAfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+AAdxAH4ADXEAfgAJcQB+\r\nAAp4AHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAA3EAfgADc3EAfgAEAAAMMHEAfgAHcQB+AA1xAH4A\r\nCXEAfgAKeABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAANxAH4AA3NxAH4ABAAADDBxAH4AB3EAfgAN\r\ncQB+AAlxAH4ACngAc3EAfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+AAdx\r\nAH4ADXEAfgAJcQB+AAp4AHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAA3EAfgADc3EAfgAEAAAMMHEA\r\nfgAHcQB+AA1xAH4ACXEAfgAKeABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAANxAH4AA3NxAH4ABAAA\r\nDDBxAH4AB3EAfgANcQB+AAlxAH4ACngAc3EAfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANzcQB+\r\nAAQAAAwwcQB+AAdxAH4ADXEAfgAJcQB+AAp4AHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAA3EAfgAD\r\nc3EAfgAEAAAMMHEAfgAHcQB+AA1xAH4ACXEAfgAKeABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAANx\r\nAH4AA3NxAH4ABAAADDBxAH4AB3EAfgANcQB+AAlxAH4ACngAc3EAfgAAP0AAAAAAAAx3CAAAABAA\r\nAAADcQB+AANzcQB+AAQAAAwwcQB+AAdxAH4ADXEAfgAJcQB+AAp4AHNxAH4AAD9AAAAAAAAMdwgA\r\nAAAQAAAAA3EAfgADc3EAfgAEAAAMMHEAfgAHcQB+AA1xAH4ACXEAfgAKeABzcQB+AAA/QAAAAAAA\r\nDHcIAAAAEAAAAANxAH4AA3NxAH4ABAAADDBxAH4AB3EAfgANcQB+AAlxAH4ACngAc3EAfgAAP0AA\r\nAAAAAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+AAdxAH4ADXEAfgAJcQB+AAp4AHNxAH4A\r\nAD9AAAAAAAAMdwgAAAAQAAAAA3EAfgADc3EAfgAEAAAMMHEAfgAHcQB+AA1xAH4ACXEAfgAKeABz\r\ncQB+AAA/QAAAAAAADHcIAAAAEAAAAANxAH4AA3NxAH4ABAAADDBxAH4AB3EAfgANcQB+AAlxAH4A\r\nCngAc3EAfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+AAdxAH4ADXEAfgAJ\r\ncQB+AAp4AHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAA3EAfgADc3EAfgAEAAAMMHEAfgAHcQB+AA1x\r\nAH4ACXEAfgAKeABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAANxAH4AA3NxAH4ABAAADDBxAH4AB3EA\r\nfgANcQB+AAlxAH4ACngAc3EAfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANzcQB+AAQAAAwwcQB+\r\nAAdxAH4ACHEAfgAJcQB+AAp4AHNxAH4AAD9AAAAAAAAMdwgAAAAQAAAAA3EAfgADc3EAfgAEAAAM\r\nMHEAfgAHcQB+AAhxAH4ACXEAfgAKeABzcQB+AAA/QAAAAAAADHcIAAAAEAAAAANxAH4AA3NxAH4A\r\nBAAADDBxAH4AB3EAfgAIcQB+AAlxAH4ACngAc3EAfgAAP0AAAAAAAAx3CAAAABAAAAADcQB+AANz\r\ncQB+AAQAAAwwcQB+AAdxAH4ACHEAfgAJcQB+AAp4AA\u003d\u003d\r\n" + }, + "potion_effects": { + "serialized_potion_effects": "" + }, + "advancements": [ + { + "key": "minecraft:recipes/transportation/mangrove_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/redstone/spruce_button", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/misc/iron_nugget_from_smelting", + "completed_criteria": { + "has_chainmail_leggings": "Oct 12, 2022, 5:43:37 PM" + } + }, + { + "key": "minecraft:recipes/redstone/spruce_pressure_plate", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/redstone/warped_pressure_plate", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/decorations/spruce_sign", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/redstone/spruce_trapdoor", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/building_blocks/warped_slab", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/redstone/spruce_door", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/redstone/spruce_fence_gate", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/decorations/crafting_table", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/decorations/chest", + "completed_criteria": { + "has_lots_of_items": "Oct 12, 2022, 5:43:42 PM" + } + }, + { + "key": "minecraft:story/shiny_gear", + "completed_criteria": { + "diamond_boots": "Oct 12, 2022, 5:43:36 PM" + } + }, + { + "key": "minecraft:recipes/misc/stick", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/redstone/warped_fence_gate", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/transportation/acacia_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/misc/iron_nugget_from_blasting", + "completed_criteria": { + "has_chainmail_leggings": "Oct 12, 2022, 5:43:37 PM" + } + }, + { + "key": "minecraft:recipes/decorations/warped_fence", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:adventure/adventuring_time", + "completed_criteria": { + "minecraft:beach": "Oct 12, 2022, 5:10:27 PM", + "minecraft:old_growth_pine_taiga": "Oct 12, 2022, 9:32:20 PM", + "minecraft:dark_forest": "Oct 11, 2022, 9:24:06 PM", + "minecraft:forest": "Oct 11, 2022, 10:06:58 PM", + "minecraft:taiga": "Oct 12, 2022, 8:58:59 PM", + "minecraft:river": "Oct 11, 2022, 10:07:07 PM", + "minecraft:stony_shore": "Oct 11, 2022, 9:23:59 PM", + "minecraft:snowy_plains": "Oct 11, 2022, 10:08:53 PM", + "minecraft:snowy_taiga": "Oct 12, 2022, 3:38:05 PM", + "minecraft:frozen_river": "Oct 11, 2022, 10:09:54 PM", + "minecraft:windswept_gravelly_hills": "Oct 12, 2022, 3:14:39 PM", + "minecraft:old_growth_spruce_taiga": "Oct 12, 2022, 9:42:12 PM", + "minecraft:snowy_beach": "Oct 11, 2022, 10:08:40 PM", + "minecraft:plains": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/decorations/barrel", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/transportation/spruce_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/redstone/redstone_from_smelting_redstone_ore", + "completed_criteria": { + "has_redstone_ore": "Oct 11, 2022, 10:21:34 PM" + } + }, + { + "key": "minecraft:recipes/transportation/birch_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/decorations/spruce_fence", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/building_blocks/spruce_stairs", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/transportation/oak_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/redstone/redstone_from_blasting_redstone_ore", + "completed_criteria": { + "has_redstone_ore": "Oct 11, 2022, 10:21:34 PM" + } + }, + { + "key": "minecraft:recipes/redstone/warped_button", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/building_blocks/warped_stairs", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/redstone/warped_trapdoor", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/building_blocks/spruce_slab", + "completed_criteria": { + "has_planks": "Oct 11, 2022, 9:25:12 PM" + } + }, + { + "key": "minecraft:recipes/decorations/warped_sign", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + }, + { + "key": "minecraft:recipes/transportation/jungle_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/transportation/dark_oak_boat", + "completed_criteria": { + "in_water": "Oct 11, 2022, 10:07:07 PM" + } + }, + { + "key": "minecraft:recipes/redstone/warped_door", + "completed_criteria": { + "has_planks": "Oct 12, 2022, 5:43:22 PM" + } + } + ], + "statistics": { + "untyped_statistics": { + "LEAVE_GAME": 16, + "TOTAL_WORLD_TIME": 282633, + "CROUCH_ONE_CM": 43, + "WALK_UNDER_WATER_ONE_CM": 113, + "DEATHS": 4, + "WALK_ONE_CM": 7313, + "JUMP": 866, + "SPRINT_ONE_CM": 63807, + "DROP_COUNT": 9, + "WALK_ON_WATER_ONE_CM": 331, + "TIME_SINCE_DEATH": 282357, + "SNEAK_TIME": 95, + "FLY_ONE_CM": 584296, + "ENDERCHEST_OPENED": 2, + "PLAY_ONE_MINUTE": 282633, + "TIME_SINCE_REST": 282377 + }, + "block_statistics": {}, + "item_statistics": { + "PICKUP": { + "SUGAR_CANE": 2 + }, + "DROP": { + "SPRUCE_PLANKS": 70, + "TURTLE_HELMET": 1, + "DIAMOND_BOOTS": 1, + "BEDROCK": 1, + "CHAINMAIL_LEGGINGS": 1, + "MANGROVE_PROPAGULE": 1, + "LEATHER_CHESTPLATE": 1 + }, + "USE_ITEM": { + "TURTLE_HELMET": 1, + "DIAMOND_BOOTS": 1, + "GRASS_BLOCK": 5, + "ENDER_CHEST": 1, + "CHAINMAIL_LEGGINGS": 1, + "LEATHER_CHESTPLATE": 1 + } + }, + "entity_statistics": {} + }, + "persistent_data_container": { + "persistent_data_map": {} + }, + "minecraft_version": "1.19.2", + "format_version": 3 +} +``` +
+ +### Dumping to the web +The Web Dump... button (equivalent to `/userdata dump web`) will dump user data snapshot json data to the https://mc.lo.gs service and provide you with a link to the uploaded file. Note that the web dumping service may not work if your user data snapshot exceeds 10MB in file size. \ No newline at end of file diff --git a/docs/Event-Priorities.md b/docs/Event-Priorities.md new file mode 100644 index 00000000..585a5aae --- /dev/null +++ b/docs/Event-Priorities.md @@ -0,0 +1,27 @@ +If you make use of plugins that perform logic with player items or statuses on the quit, join or death events, such as combat logging plugins, you may encounter issues with HuskSync caused by the event execution order. + +In the case of combat logging plugins, this can mean that HuskSync is listening to the event called when a player dies, joins or leaves before the combat logger can kill the player and handle their items. In other words, the player will be brought back to life and synchronized as though they didn't die, even though. This can lead to item duplication. + +HuskSync provides a way of customizing the event priorities—that is, the priorities at which HuskSync listens to event calls—to let you fix this issue. + +## Changing event priorities +As of HuskSync v2.1.3+, you can modify event priorities by editing the `synchronization` section of the `config.yml` file, as seen below. + +```yaml +synchronization: + #(...) + event_priorities: + join_listener: LOWEST + death_listener: NORMAL + quit_listener: LOWEST +``` + +To change the event execution priority for the join, death or quit listener, simply modify the value to one of the ones listed below, in order of when they are processed: +1. `LOWEST` (executed first, just after the event is fired) +2. `NORMAL` (executed after all LOWEST listeners have finished processing) +3. `HIGHEST` (executed after all NORMAL and LOWEST listeners have finished processing) + +Note that by default, HuskSync executes the join and quit events on the earliest listener priority (`LOWEST`). This is for synchronization performance reasons; in the case of the quit event listener, the earlier in the disconnect process HuskSync can save data, the better. This is because some plugins can be taxing on the tick cycle of the server, causing delays in data syncing which reduces the seamlessness of the system. + +## Combat-loggers +For those using combat logging plugins—the ones that kill players when they disconnect while in PvP—you should try changing the `quit_listener` to having a `NORMAL` or `HIGHEST` priority. \ No newline at end of file diff --git a/docs/FAQs.md b/docs/FAQs.md new file mode 100644 index 00000000..29b82c69 --- /dev/null +++ b/docs/FAQs.md @@ -0,0 +1,54 @@ +This page addresses a number of frequently asked questions about the plugin. + +## Frequently Asked Questions + +
+ What data can be synchronised? + +HuskSync supports synchronising a wide range of different data elements, each of which can be toggled to your liking. Please check out the [[Sync Features]] page for a full list. + +
+ +
+ Is Redis required? What is Redis? + +HuskSync requires Redis to operate (for reasons demonstrated below). Redis is an in-memory database server used for caching data at scale and sending messages across a network. You have a Redis server in a similar fashion to the way you have a MySQL database server. If you're using a Minecraft hosting company, you'll want to contact their support and ask if they offer Redis. If you're looking for a host, I have a list of some popular hosts and whether or not they support Redis [available to read here.](https://william278.net/redis-hosts) + +
+ +
+ How does the plugin synchronise data? + +[![System diagram](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/system-diagram.png)](#) + +HuskSync makes use of both MySQL and Redis for optimal data synchronisation. + +When a user changes servers, in addition to data being saved to MySQL, it is also cached via the Redis server with a temproary expiry key. When changing servers, the receiving server detects the key and sets the user data from Redis. When a player rejoins the network, the system fetches the last-saved data snapshot from the MySQL Database. + +This approach is able to dramatically improve both synchronisation performance and reliability. A few other techniques are used to optimize this process, such as comrpessing the serialized user data json using Snappy. + +
+ +
+ Why doesn't HuskSync sync player economy balances / support Vault? + +This is a very common request, but there's a good reason why HuskSync does not support this. + +The Vault API is designed to be a central "Vault" for storing user data. It's the role of economy plugins that *implement* vault to handle the data storage -- and, by extension, synchronization cross-server. Plugins that *hook into* Vault then expect to be able to use the Vault API to get the player's latest economy balance and data. + +Plugins such as MySQLPlayerDataBridge that support synchronizing Vault *hook into* Vault and as a result can violate this expectation—plugins that expect Vault to return the latest user data no longer can. As a result, plugins like MySQLPlayerDataBridge have to provide lots of manual hooks and tweaks for individual plugins to ensure compatibility. + +This causes all sorts of compatibility issues with unsupported plugins and increases plugin size and update workload. + +As a result, I recommend using an economy plugin (that directly *implements* the Vault API), that works cross-server. XConomy is a popular choice for this, which I have personally had a good experience with in the past. + +
+ +
+ Is this better than MySQLPlayerDataBridge? + +I can't provide a fair answer to this question! What I can say is that your mileage may vary. The performance improvements offered by HuskSync's synchronisation method will depend on your network environment and the economies of scale that come with your player count. + +A migrator from MPDB is built-in to HuskSync. + +
\ No newline at end of file diff --git a/docs/Home.md b/docs/Home.md new file mode 100644 index 00000000..d9535eba --- /dev/null +++ b/docs/Home.md @@ -0,0 +1,31 @@ +# [![HuskSync banner](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/banner.png)](https://github.com/WiIIiam278/HuskSync) +Welcome! This is the plugin documentation for HuskSync v2.x+. Please click through to the topic you'd like to read about. + +## Guides +* 📚 [[Setup]] +* 📄 [[Config File]] +* 🔗 [[Troubleshooting]] +* ↪️ [[Data Rotation]] +* ↗️ [[Legacy Migration]] +* ✨ [[MPDB Migration]] +* 🎏 [[Translations]] +* ❓ [[FAQs]] + +## Documentation +* 🖥️ [[Commands]] +* ✅ [[Sync Features]] +* 🟩 [[Plan Hook]] +* ☂️ [[Dumping UserData]] +* 📋 [[Event Priorities]] +* ⚔️ [[Keep Inventory]] +* 📦 [[API]] + * 📝 [[UserData API]] + * ❗ [[API Events]] + +## Links +* 💻 [GitHub](https://github.com/WiIIiam278/HuskSync) +* 📂 [Buy HuskSync](https://www.spigotmc.org/resources/husksync.97144/) + * 🛒 [Spigot](https://www.spigotmc.org/resources/husksync.97144/) + * 🛒 [Polymart](https://polymart.org/resource/husksync.1634) + * 🛒 [Songoda](https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758) +* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG) \ No newline at end of file diff --git a/docs/Keep-Inventory.md b/docs/Keep-Inventory.md new file mode 100644 index 00000000..7cb54c38 --- /dev/null +++ b/docs/Keep-Inventory.md @@ -0,0 +1,33 @@ +If your server uses the `keepInventory` gamerule, where players keep the contents of their inventory after dying, HuskSync's built-in snapshot-on-death and dead-player synchronisation features can cause a conflict leading to synchronisation issues. + +To solve this issue, you will need to adjust three settings in your `config.yml` file, as described below. + +## Why does this happen? +HuskSync has some special handling when players die, to account for scenarios where users change servers after death (to prevent item loss). +* **Death state saving**—HuskSync has special logic to save player snapshots *except their inventory* when they change servers while dead. When `keepInventory` is enabled, though, the inventory still contains items, so the snapshot is not saved correctly. This logic is enabled by default. +* **Snapshot creation on death**—HuskSync can create a special snapshot for backup purposes when a player dies, formed by taking their drops and setting this to their inventory. When `keepInventory` is enabled, the player drops are empty, so this creates an inaccurate snapshot. This option is disabled by default. + +## How can this be fixed? +You will need to set the `synchronization.save_on_death` (which controls making snapshots on death), `save_empty_drops_on_death` (which controls whether snapshots of players who have no items to drop should be created), and `synchronization.synchronise_dead_players_changing_server` (which controls whether to sync dead players when they change servers) options to `false` in `config.yml`. + +
+ Example in config.yml + + ```yml + synchronization: + # ... + save_on_death: false # <-- Set this to false + save_empty_drops_on_death: false # <-- Set this to false + # ... + synchronise_dead_players_changing_server: false # <-- Set this to false + ``` + +
+ + +## Troubleshooting with custom keepInventory setups +If the above doesn't work for you, you may need to do more things to get this to work properly. + +If your server uses an advanced custom setup where some items are kept and others are not through custom plugin logic, you'll need to use the HuskSync API to create a custom hook to update data on the DataSaveEvent when a player changes server *while dead*, transforming their inventory data as appropriate. + +If your server uses a permission node to control whether the user keeps their inventory on death, you should be able to follow the above instructions although your mileage may vary dependent on your setup and how you handle players when they die. Note that this option may also conflict with other plugins that make assumptions about the persistence of items on death. \ No newline at end of file diff --git a/docs/Legacy-Migration.md b/docs/Legacy-Migration.md new file mode 100644 index 00000000..be00f3cf --- /dev/null +++ b/docs/Legacy-Migration.md @@ -0,0 +1,30 @@ +This guide will walk you through how to upgrade from HuskSync v1.4.x to HuskSync 2.x. + +## Requirements +- MySQL Database with HuskSync v1.4.x data + - Migration from SQLite is not supported, as HuskSync v2.x requires a MySQL database and does not support SQLite. Appologies for the inconvenience. + - If you're running v1.3.x or older, follow the update instructions to 1.4.x first before updating to 2.x. + +## Migration Instructions +### 1. Uninstall HuskSync v1.x from all servers +- Switch off all servers and your proxy +- Delete the jarfile from your `~/plugins/` folders on your Spigot servers +- Also delete the jarfile from your `~/plugins/` folders on your Proxy. HuskSync v2.x no longer requires a proxy plugin. +- Delete (or make a copy and delete) all HuskSync config data folders (`~/plugins/HuskSync/`). HuskSync 2.x has a new config and messages file. + +### 2. Install HuskSync v2.x on all Spigot servers +- HuskSync v2.x must only be installed on your Spigot servers, not your proxy. +- Follow the setup instructions [here](Setup). + +### 3. Configure the migrator +- With your servers back on and correctly configured to run HuskSync v2.x, ensure nobody is online. +- Use the console on one of your Spigot servers to enter: `husksync migrate legacy` +- Carefully read the migration configuration instructions. In most cases, you won't have to change the settings, but if you do need to adjust them, use `husksync migrate legacy set `. +- Migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. If you're migrating from multiple clusters, ensure you run the migrator on the correct servers corresponding to the migrator. + +### 4. Start the migrator +- Run `husksync migrate legacy start` to begin the migration process. This may take some time, depending on the amount of data you're migrating. + +### 5. Ensure the migration was successful +- HuskSync will notify in console when migration is complete. Verify that the migration went OK by logging in and using the `/userdata list ` command to see if the data was imported with the `legacy migration` cause. +- You can delete the old tables in the database if you want. Be careful to make sure you delete the right ones. By default the *new* table names are `husksync_users` and `husksync_user_data` and the *old* ones were `husksync_players` and `husksync_data`, but you may have changed these. \ No newline at end of file diff --git a/docs/MPDB-Migration.md b/docs/MPDB-Migration.md new file mode 100644 index 00000000..fb499d89 --- /dev/null +++ b/docs/MPDB-Migration.md @@ -0,0 +1,28 @@ +This guide will walk you through how to migrate from MySQLPlayerDataBridge (MPDB) to HuskSync v2.x. + +## Requirements +- Spigot servers with MySQLPlayerDataBridge *still installed* + +## Migration Instructions +### 1. Install HuskSync v2.x on all Spigot servers +- Download, then install HuskSync on all your servers. Don't uninstall MySQLPlayerDataBridge yet. +- Follow the setup instructions [here](Setup). +- Start your servers again when done. + +### 2. Configure the migrator +- With your servers back on and correctly configured to run HuskSync v2.x, ensure nobody is online. +- Use the console on one of your Spigot servers to enter: `husksync migrate mpdb`. If the MPDB migrator is not available, ensure MySQLPlayerDataBridge is still installed. +- Adjust the migration setting as needed using the following command: `husksync migrate mpdb set `. +- Note that migration will be carried out *from* the database you specify with the settings in console *to* the database configured in `config.yml`. + +### 3. Start the migrator +- Run `husksync migrate mpdb start` to begin the migration process. This may take some time, depending on the amount of data you're migrating. + +### 4. Uninstall MySQLPlayerDataBridge +- HuskSync will display a message in console when data migration is complete. +- Stop all your Spigot servers and remove the MySQLPlayerDataBridge jar from each of them. +- Start your Spigot servers again. + +### 5. Ensure the migration was successful +- Verify that the migration was successful by logging in and using the `/userdata list ` command to see if the data was imported with the `mpdb_migration` cause. +- You can delete the old tables in the database if you want. Be careful to make sure you delete the correct ones. \ No newline at end of file diff --git a/docs/Plan-Hook.md b/docs/Plan-Hook.md new file mode 100644 index 00000000..85d702d8 --- /dev/null +++ b/docs/Plan-Hook.md @@ -0,0 +1,13 @@ +HuskSync supports displaying information about user data on your [Player Analytics](https://github.com/plan-player-analytics/Plan) (Plan) web panel. + +[![Plan hook screenshots](https://raw.githubusercontent.com/WiIIiam278/HuskSync/master/images/plan-hook.png)](#) + +## Requirements +- HuskSync v2.0+ +- Plan v5.4.1690+ + +## Setup +1. Install Plan to your Spigot server(s) with HuskSync installed +2. Configure Plan as necessary and restart your servers +3. Data will start showing up on Player pages on the "Plugins" panel. +4. Click between the two tabs to view a preview of the user's current data, and a list of their user data snapshots. \ No newline at end of file diff --git a/docs/Setup.md b/docs/Setup.md new file mode 100644 index 00000000..ffbe7439 --- /dev/null +++ b/docs/Setup.md @@ -0,0 +1,25 @@ +This will walk you through installing HuskSync on your network of Spigot servers. + +## Requirements +* A MySQL Database (v8.0+) +* A Redis Database (v5.0+) — see [[FAQs]] for more details. +* Any number of Spigot servers, connected by a BungeeCord or Velocity-based proxy (Minecraft v1.16.5+, running Java 16+) + +## Setup Instructions +### 1. Install the jar +- 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. Restart servers +- Start, then stop every server to let HuskSync generate the [[config file]]. +- HuskSync will throw an error in console and disable itself as it is unable to connect to the database. You haven't set the credentials yet, so this is expected. +- Advanced users: If you'd prefer, you can just create one config.yml file and create symbolic links in each `/plugins/HuskSync/` folder to it to make updating it easier. +### 3. Enter MySQL & Redis database credentails +- Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) +- Under `credentials` in the `database` section, enter the credentials of your MySQL Database. You shouldn't touch the `connection_pool` properties. +- Under `credentials` in the `redis` section, enter the credentails of your Redis Database. If your Redis server doesn't have a password, leave the password blank as it is. +- Unless you want to have multiple clusters of servers within your network, each with separate user data, do not change the value of `cluster_id`. +### 4. Start every server again +- Provided your MySQL and Redis credentials were correct, synchronisation should begin as soon as you start your servers again. +- If you need to import data from HuskSync v1.x or MySQLPlayerDataBridge, please see the guides below: + - [[Legacy Migration]] + - [[MPDB Migration]] \ No newline at end of file diff --git a/docs/Sync-Features.md b/docs/Sync-Features.md new file mode 100644 index 00000000..e9659472 --- /dev/null +++ b/docs/Sync-Features.md @@ -0,0 +1,72 @@ +This page contains a list of the features HuskSync is and isn't able to syncrhonise on your server. + +You can customise how much data HuskSync saves about a player by [turning each synchronisation feature on or off](#toggling-sync-features). When a synchronisation feature is turned off, HuskSync won't touch that part of a player's profile; in other words, the data they will inherit when changing servers will be read from their player data file on the local server. + +## Feature table +✅—Supported  ❌—Unsupported  ⚠️—Experimental + +| Name | Description | Availability | +|---------------------------|-------------------------------------------------------------|:------------:| +| Inventories | Items in player inventories & selected hotbar slot | ✅ | +| Ender chests | Items in ender chests* | ✅ | +| Health | Player health points | ✅ | +| Max health | Player max health points and health scale | ✅ | +| Hunger | Player hunger, saturation & exhaustion | ✅ | +| Experience | Player level, experience points & score | ✅ | +| Potion effects | Active status effects on players | ✅ | +| Advancements | Player advancements, recipes & progress | ✅ | +| Game modes | Player's current game mode | ✅ | +| Statistics | Player's in-game stats (ESC -> Statistics) | ✅ | +| Location | Player's current coordinate positon and world† | ✅ | +| Persistent Data Container | Custom plugin persistent data key map | ⚠️ | +| Locked maps | Maps/treasure maps locked in a cartography table | ⚠️ | +| Unlocked maps | Regular, unlocked maps/treasure maps ([why?](#map-syncing)) | ❌ | +| Economy balances | Vault economy balance. ([why?](#economy-syncing)) | ❌ | + +*Purpur's custom ender chest resizing feature is also supported. + +†This is intended for servers that have mirrorred worlds across instances (such as RPG servers). With this option enabled, players will be placed at the same coordinates when changing servers. + +### PersistentDataContainer tags +The player [PersistentDataContainer](https://blog.jeff-media.com/persistent-data-container-the-better-alternative-to-nbt-tags/) is a part of the Spigot API that enables plugins to set custom data tags to players, entities & items and have them persist. HuskSync will synchronise this data cross-server. Plugins that use legacy or propietary forms of saving data, such as by modifying NBT directly, may not correctly synchronise. + +### Custom enchantments +Plugins that add custom enchantments by registering them to ItemStacks through setting them via the [EnchantmentStorageMeta](https://hub.spigotmc.org/javadocs/spigot/org/bukkit/inventory/meta/EnchantmentStorageMeta.html) will work, but note that the plugin _must_ be lower on the load order than HuskSync; in other words, HuskSync should be on the plugin's `loadbefore:`. This is because Spigot's item serialization API requires that the plugin that registered the enchantment be online to serialize it due to how it reads from the enchantment registry and so if the plugin does not load before (and thus does not shut down after) HuskSync, it won't be able to serialize the custom enchantments in the event of a server shutdown with players online. + +### Map syncing +Map items are a special case, as their data is not stored in the item itself, but rather in the game world files. In addition to this, their data is dynamic and changes based on the updating of the world, something which can't be tracked across multiple instances. As a result, it's not possible to sync unlocked map items. + +However, experimental support for synchronising locked map items—that is, maps that have been locked in a cartography table—is currently available in development builds. This works by serializing its' map canvas pixel grid to the map item's persistent data container. + +### Economy syncing +Although it's a common request, HuskSync doesn't synchronise economy data for a number of reasons! + +I strongly reccommend making use of economy plugins that have cross-server economy balance synchronisation built-in, of which there are a multitude of options available. Please see our [[FAQs]] section for more details on this decision. + +## Toggling Sync Features +All synchronisation features, except location and locked map synchronising, are enabled by default. To toggle a feature, navigate to the `features:` section in the `synchronisation:` part of your `config.yml` file, and change the option to `true`/`false` respectively. + +
+ Example in config.yml + + ```yaml + synchronization: + # ... + features: + inventories: true + ender_chests: true + health: true + max_health: true + hunger: true + experience: true + potion_effects: true + advancements: true + game_mode: true + statistics: true + persistent_data_container: false + locked_maps: true + location: false + #... + ``` + +
\ No newline at end of file diff --git a/docs/Translations.md b/docs/Translations.md new file mode 100644 index 00000000..a6879a84 --- /dev/null +++ b/docs/Translations.md @@ -0,0 +1,14 @@ +HuskSync supports a number of community-sourced translations of the plugin locales into different languages. The default language is [`en-gb`](https://github.com/WiIIiam278/HuskSync/blob/master/common/src/main/resources/locales/en-gb.yml) (English). The messages file is formatted using [MineDown](https://github.com/Phoenix616/MineDown). + +You can change which preset language option to use by changing the top-level `language` setting in the plugin config.yml file. You must change this to one of the supported language codes. You can [view a list of the supported languages](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales) by looking at the locales source folder. + +## Contributing Locales +You can contribute locales by submitting a pull request with a yaml file containing translations of the [default locales](https://github.com/WiIIiam278/HuskSync/blob/master/common/src/main/resources/locales/en-gb.yml) into your language. Here's a few pointers for doing this: +* Do not translate the locale keys themselves (e.g. `teleporting_offline_player`) +* Your pull request should be for a file in the [locales folder](https://github.com/WiIIiam278/HuskSync/tree/master/common/src/main/resources/locales) +* Do not translate the [MineDown](https://github.com/Phoenix616/MineDown) markdown syntax itself or commands and their parameters; only the english interface text +* Each locale should be on one line, and the header should be removed. +* Use the correct ISO 639-1 [locale code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language and dialect +* If you are able to, you can add your name to the `AboutMenu` translators credit list yourself, otherwise this can be done for you + +Thank you for your interest in making HuskSync more accessible around the world! \ No newline at end of file diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 00000000..ced80278 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,24 @@ +This page contains a number of common issues and how you can troubleshoot and resolve them. + +## Topics +### Duplicate UUIDs in database +This is most frequently caused by running a cracked "offline mode" network of servers. We [don't provide support](https://william278.net/terms) for problems caused by cracked servers and the most advice we can offer you is: +- Ensure `bungee_online_mode` is set to the correct value in the `paper.yml` config file on each of your Bukkit servers +- Ensure your authenticator plugin is passing valid, unique IDs to each backend Spigot server. + +### Cannot set data with newer Minecraft version than the server +This is caused when you attempt to downgrade user data from a newer version of Minecraft to an older one, or when your Spigot servers are running mismatched Minecraft versions. + +HuskSync will identify this and safely prevent the synchronisation from occuring. Your Spigot servers must be running the same version of both Minecraft and HuskSync. + +### User data failing to synchronise +This can occur due to misaligned timings between your Spigot servers and your Redis server. HuskSync has a built in way of tuning this. Try continously increasing the `network_latency_milliseconds` option in your config to a higher value. + +### Synchronisation issues with Keep Inventory enabled +On servers that use Keep Inventory move (where players keep their items when they die), you can run into synchronisation issues. See [[Keep Inventory]] for details on why this happens and how to resolve it. + +### Exceptions when compressing data via Snappy (lightweight Linux distros) +Some lightweight Linux distros such as Alpine Linux (used on Pterodactyl) might not have the dependencies needed for the [Snappy](https://github.com/xerial/snappy-java) compressor. It's possible to disable data compression by changing `compress_data` to false in your config. Note that after changing this setting you will need to reset your database. Alternatively, find the right libraries for your distro! + +### Redis connection problems on Pterodactyl +The internal firewall on Pterodactyl can block Redis connections between servers. Add an allocation to each server allowing them to communicate with your Redis server. It may be easier to install Redis in an egg than trying to use the backend internal service. \ No newline at end of file diff --git a/docs/UserData-API.md b/docs/UserData-API.md new file mode 100644 index 00000000..5fc78400 --- /dev/null +++ b/docs/UserData-API.md @@ -0,0 +1,243 @@ +HuskSync provides an API for fetching and retrieving `UserData`; a snapshot of a user's synchronization. + +This page assumes you've read the [[API]] introduction and have imported the HuskSync API into your repository. + +## Table of contents +1. Creating a class to interface with the API +2. Checking if HuskSync is present and creating the hook +3. Getting an instance of the API +4. Getting a user by UUID +5. Getting a user's data +6. Getting a user's data +7. Getting a user's inventory contents +8. Updating a user's data + +## 1. Creating a class to interface with the API +- Unless your plugin completely relies on HuskSync, you shouldn't put HuskSync API calls into your main class, otherwise if HuskSync is not installed you'll encounter `ClassNotFoundException`s + +```java +public class HuskSyncAPIHook { + + public HuskSyncAPIHook() { + // Ready to do stuff with the API + } + +} +``` +## 2. Checking if HuskSync is present and creating the hook +- Check to make sure the HuskSync plugin is present before instantiating the API hook class + +```java +public class MyPlugin extends JavaPlugin { + + public HuskSyncAPIHook huskSyncAPIHook; + + @Override + public void onEnable() { + if (Bukkit.getPluginManager().getPlugin("HuskSync") != null) { + this.huskSyncAPIHook = new HuskSyncAPIHook(); + } + } +} +``` + +## 3. Getting an instance of the API +- You can now get the API instance by calling `HuskSyncAPI#getInstance()` + +```java +import net.william278.husksync.api.HuskSyncAPI; + +public class HuskSyncAPIHook { + + private final HuskSyncAPI huskSyncAPI; + + public HuskSyncAPIHook() { + this.huskSyncAPI = HuskSyncAPI.getInstance(); + } + +} +``` + +## 4. CompletableFuture and Optional basics +- HuskSync's API methods return `CompletableFuture`s and `Optional`s. +- A `CompletableFuture` is an asynchronous callback mechanism. The method will be processed asynchronously and the data returned when it has been retrieved. While you can use `CompletableFuture#join()` to block the thread until the future has finished processing, it's smarter to use `CompletableFuture#thenAccept(data -> {})` to do what you want to do with the `data` you requested after it has asynchronously been retrieved, to prevent lag. +- An `Optional` is a null-safe representation of data, or no data. You can check if the Optional is empty via `Optional#isEmpty()` (which will be returned by the API if no data could be found for the call you made). If the optional does contain data, you can get it via `Optional#get()`. + +## 5. Getting a user by UUID +- HuskSync has a `User` object, representing a user saved in the database. You can retrieve a user using `HuskSyncAPI#getUser(uuid)` +- If you have an online `org.bukkit.Player` object, you can use `BukkitPlayer#adapt(player)` to get an `OnlineUser` (extends `User`), representing a logged-in user. + +```java +public class HuskSyncAPIHook { + + private final HuskSyncAPI huskSyncAPI; + + public HuskSyncAPIHook() { + this.huskSyncAPI = HuskSyncAPI.getInstance(); + } + + + public void logUserName(UUID uuid) { + // getUser() returns a CompletableFuture supplying an Optional + huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> { + // Check if we found a user by that UUID either online or on the database + if (optionalUser.isEmpty()) { + // If we didn't, we'll log that they don't exist + System.out.println("User does not exist!"); + return; + } + // The User object has two fields; uuid and username. + System.out.println("User name is: " + optionalUser.get().username); + }); + } + +} +``` + +## 6. Getting a user's data +- With a `User` object, we can now call `HuskSyncAPI#getUserData()` to fetch their latest data +- The `UserData` object contains eight data "modules", each holding certain parts of information. +- UserData does not have to contain any single data "module"; the modules contained in a given UserData object when user data is saved by the plugin are determined by the plugin config settings. +- You can fetch each module, which returns wrapped in an optional (empty if not present in the UserData object), via: + - `UserData#getStatus();` - The user's status (health, hunger, saturation, exp, game mode, etc) + - `UserData#getInventory();` - The user's inventory contents. Contains a Base 64 serialized `ItemStack` array. + - `UserData#getEnderChest();` - The user's ender chest contents. Contains a Base 64 serialized `ItemStack` array. + - `UserData#getPotionEffects();` - The user's active potion effects. Contains a Base 64 serialized `PotionEffect` array. + - `UserData#getAdvancements();` - List of a user's advancements and mapped awarded criteria + - `UserData#getStatistics();` - The user's statistics data containing four categories (untyped, items, blocks and entities) + - `UserData#getLocation();` - The user's location data, for servers that have location syncing turned on. + - `UserData#getPersistentDataContainer();` - The user's peristent data container, containing a map of keys to strings + +```java +public class HuskSyncAPIHook { + + // ... // + + public void logUserData(UUID uuid) { + huskSyncAPI.getUser(uuid).thenAccept(optionalUser -> { + // Optional#isPresent() is the opposite of #isEmpty() + if (optionalUser.isPresent()) { + logHealth(optionalUser.get()); + } + }); + } + + private void logHealth(User user) { + // A user might not have data, if it's deleted by an admin or they're brand new + huskSyncAPI.getUserData(user).thenAccept(optionalUserData -> { + if (optionalUserData.isPresent()) { + // Get the StatusData from the UserData object + Optional statusData = optionalUserData.get().getStatus(); + // Print the health from the fields, if the user has a status object + statusData.ifPresent(status -> { + System.out.println(user.username + "'s health: " + status.health + "/" + status.maxHealth); + }); + } + }); + } + +} +``` + +## 7. Getting a user's inventory contents +- The API provides methods for deserialzing `ItemData` used to hold Base 64 serialized inventory and ender chest `ItemStack` array contents into actual `ItemStack` array data. +- For deserialziing inventories, use `HuskSyncAPI#deserializeInventory(serializedItems)` +- For deserialziing ender chests, use `HuskSyncAPI#deserializeItemStackArray(serializedItems)` +- Alternatively, the `HuskSyncAPI#getPlayerInventory(user)` and `HuskSyncAPI#getPlayerEnderChest(user)` methods will do this for you nice and easily. Note though if the last UserData module does not contain UserData, the ItemData returned here will be representative of an empty ItemStack array. +- Serialization and deserialization methods are also available for Potion Effects. + +```java +public class HuskSyncAPIHook { + + // ... // + + private void printInventoryItems(User user) { + huskSyncAPI.getUserData(user).thenAccept(optionalUserData -> { + if (optionalUserData.isPresent()) { + // Get the ItemData and make sure it's present + Optional inventoryDataOptional = optionalUserData.get().getInventory(); + if (inventoryDataOptional.isEmpty) { + return; + } + ItemData inventoryData = inventoryDataOptional.get(); + + // Get the ItemStack[] array as a BukkitInventoryMap. + // This returns a future, but we're using #join() to block the thread until it's done + BukkitInventoryMap inventory = huskSyncAPI.deserializeInventory(inventoryData.serializedItems).join(); + + // A BukkitInventoryMap is simply a wrapper for an ItemStack array. + // It provides a few handy methods for getting the player's armor, their offhand item, etc. + // To get the ItemStack array from it, just call BukkitInventoryMap#getContents(); + for (ItemStack item : inventory.getContents()) { + // Print out the item material types of every item in the player's inventory + System.out.println(item.getType().name()); + } + } + }); + } + +} +``` + +
+HuskSyncAPI#getPlayerInventory() + +```java +private void printInventoryItems(User user) { + huskSyncAPI.getPlayerInventory(user).thenAccept(inventory -> { + if (inventory.isPresent()) { + for (ItemStack item : inventory.get().getContents()) { + System.out.println(item.getType().name()); + } + } + }); +} +``` +
+ +
+HuskSyncAPI#getPlayerEnderChest() + +```java +private void printEnderChestItems(User user) { + huskSyncAPI.getPlayerEnderChest(user).thenAccept(enderChest -> { + if (enderChest.isPresent()) { + for (ItemStack item : enderChest.get()) { + System.out.println(item.getType().name()); + } + } + }); +} +``` +
+ + +## 8. Updating a user's data +- You can use `HuskSyncAPI#setUserData(user, userData)` to set a user's modified data to the database. +- If you need to modify user data every time it's updated, you may want to look at listening to one of HuskSync's provided events instead. +- You can use `HuskSyncAPI#serializeItemStackArray(itemStack[])` to serialize an array of ItemStacks into Base 64. +- Alternatively, you can use `HuskSyncAPI#setInventoryData(user, bukkitInventoryMap)` to set a user's inventory, or `HuskSyncAPI#setEnderChestData(user, itemStack[])` to set a user's ender chest. +- Updating UserData will overwrite the player's current "live" data. HuskSync does not track the player's "live" data constantly by design, only "snapshots" of their data when it is saved. In other words, getting and updating a user's data may actually end up rolling them back. + +```java +public class HuskSyncAPIHook { + + // Set a user's health to 20 + private void updateUserHealth(User user) { + huskSyncAPI.getUserData(user).thenAccept(optionalUserData -> { + if (optionalUserData.isPresent()) { + UserData data = optionalUserData.get(); + Optional statusDataOptional = data.getStatus(); + StatusData statusData = statusDataOptional.get(); + statusData.health = 20; + + // This returns a CompletableFuture that will invoke #thenRun() when it has completed + huskSyncAPI.setUserData(user, data).thenRun(() -> { + System.out.println("Healed " + user.username + "!"); + }); + } + }); + } + +} +``` \ No newline at end of file diff --git a/docs/_Footer.md b/docs/_Footer.md new file mode 100644 index 00000000..bdbaefba --- /dev/null +++ b/docs/_Footer.md @@ -0,0 +1,2 @@ +| This documentation is available via [william278.net](https://william278.net/docs/husksync/Home) | +| ----------------------------------------------------------------------------------------------- | \ No newline at end of file diff --git a/docs/_Sidebar.md b/docs/_Sidebar.md new file mode 100644 index 00000000..b8e87a76 --- /dev/null +++ b/docs/_Sidebar.md @@ -0,0 +1,28 @@ +## Guides +* 📚 [[Setup]] +* 📄 [[Config File]] +* 🔗 [[Troubleshooting]] +* ↪️ [[Data Rotation]] +* ↗️ [[Legacy Migration]] +* ✨ [[MPDB Migration]] +* 🎏 [[Translations]] +* ❓ [[FAQs]] + +## Documentation +* 🖥️ [[Commands]] +* ✅ [[Sync Features]] +* 🟩 [[Plan Hook]] +* ☂️ [[Dumping UserData]] +* 📋 [[Event Priorities]] +* ⚔️ [[Keep Inventory]] +* 📦 [[API]] + * 📝 [[UserData API]] + * ❗ [[API Events]] + +## Links +* 💻 [GitHub](https://github.com/WiIIiam278/HuskSync) +* 📂 [Buy HuskSync](https://www.spigotmc.org/resources/husksync.97144/) + * 🛒 [Spigot](https://www.spigotmc.org/resources/husksync.97144/) + * 🛒 [Polymart](https://polymart.org/resource/husksync.1634) + * 🛒 [Songoda](https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758) +* 💬 [Discord Support](https://discord.gg/tVYhJfyDWG) \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 9942991e..e488ac69 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,8 +3,9 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=16 -plugin_version=2.2.4 +plugin_version=2.2.5 plugin_archive=husksync +plugin_description=A modern, cross-server player data synchronization system jedis_version=4.3.2 mysql_driver_version=8.0.32