Add workflow files, test reporting, maven publishing, docs, bump version

feat/data-edit-commands
William 2 years ago
parent 7bb4bff485
commit 0fae3484a1
No known key found for this signature in database

@ -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'

@ -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

@ -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

@ -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'

@ -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]'

@ -0,0 +1,10 @@
This file is part of HuskSync by William278. Do not redistribute!
Copyright (c) William278 <will27528@gmail.com>
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.

@ -1,8 +1,8 @@
<!--suppress ALL --> <!--suppress ALL -->
<p align="center"> <p align="center">
<img src="images/banner.png" alt="HuskSync" /> <img src="images/banner.png" alt="HuskSync" />
<a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/java_ci.yml"> <a href="https://github.com/WiIIiam278/HuskSync/actions/workflows/ci.yml">
<img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/java_ci.yml?branch=master&logo=github"/> <img src="https://img.shields.io/github/actions/workflow/status/WiIIiam278/HuskSync/ci.yml?branch=master&logo=github"/>
</a> </a>
<a href="https://jitpack.io/#net.william278/HuskSync"> <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" /> <img src="https://img.shields.io/jitpack/version/net.william278/HuskSync?color=%2300fb9a&label=api&logo=gradle" />

@ -1,15 +1,19 @@
plugins { plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1' id 'com.github.johnrengelman.shadow' version '8.1.1'
id 'org.ajoberstar.grgit' version '5.0.0' id 'org.cadixdev.licenser' version '0.6.1' apply false
id 'java' id 'org.ajoberstar.grgit' version '5.2.0'
id 'maven-publish' id 'maven-publish'
id 'java'
} }
group 'net.william278' group 'net.william278'
version "$ext.plugin_version-${versionMetadata()}" version "$ext.plugin_version-${versionMetadata()}"
description "$ext.plugin_description"
defaultTasks 'licenseFormat', 'build'
ext { ext {
set 'version', version.toString() set 'version', version.toString()
set 'description', description.toString()
set 'jedis_version', jedis_version.toString() set 'jedis_version', jedis_version.toString()
set 'mysql_driver_version', mysql_driver_version.toString() set 'mysql_driver_version', mysql_driver_version.toString()
set 'snappy_version', snappy_version.toString() set 'snappy_version', snappy_version.toString()
@ -20,6 +24,7 @@ import org.apache.tools.ant.filters.ReplaceTokens
allprojects { allprojects {
apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'org.cadixdev.licenser'
apply plugin: 'java' apply plugin: 'java'
compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8'
@ -48,6 +53,12 @@ allprojects {
useJUnitPlatform() useJUnitPlatform()
} }
license {
header = rootProject.file('HEADER')
include '**/*.java'
newLine = true
}
processResources { processResources {
filter ReplaceTokens as Class, beginToken: '${', endToken: '}', filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
tokens: rootProject.ext.properties tokens: rootProject.ext.properties
@ -58,6 +69,10 @@ subprojects {
version rootProject.version version rootProject.version
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}" archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
jar {
from '../LICENSE'
}
if (['bukkit', 'plugin'].contains(project.name)) { if (['bukkit', 'plugin'].contains(project.name)) {
shadowJar { shadowJar {
destinationDirectory.set(file("$rootDir/target")) destinationDirectory.set(file("$rootDir/target"))
@ -79,6 +94,35 @@ subprojects {
shadowJar.dependsOn(sourcesJar, javadocJar) shadowJar.dependsOn(sourcesJar, javadocJar)
publishing { 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 { publications {
mavenJava(MavenPublication) { mavenJava(MavenPublication) {
groupId = 'net.william278' groupId = 'net.william278'

@ -1,13 +1,13 @@
name: HuskSync name: 'HuskSync'
version: ${version} version: '${version}'
main: net.william278.husksync.BukkitHuskSync main: 'net.william278.husksync.BukkitHuskSync'
api-version: 1.16 api-version: 1.16
author: William278 author: 'William278'
description: 'A modern, cross-server player data synchronization system' description: '${description}'
website: 'https://william278.net' website: 'https://william278.net'
softdepend: softdepend:
- MysqlPlayerDataBridge - 'MysqlPlayerDataBridge'
- Plan - 'Plan'
libraries: libraries:
- 'redis.clients:jedis:${jedis_version}' - 'redis.clients:jedis:${jedis_version}'
- 'com.mysql:mysql-connector-j:${mysql_driver_version}' - 'com.mysql:mysql-connector-j:${mysql_driver_version}'
@ -16,14 +16,14 @@ libraries:
commands: commands:
husksync: husksync:
usage: '/husksync <update/info/reload/migrate>' usage: '/<command> <update/info/reload/migrate>'
description: 'Manage the HuskSync plugin' description: 'Manage the HuskSync plugin'
userdata: userdata:
usage: '/userdata <view/list/delete/restore/pin/dump> <username> [version_uuid]' usage: '/<command> <view/list/delete/restore/pin/dump> <username> [version_uuid]'
description: 'View, manage & restore player userdata' description: 'View, manage & restore player userdata'
inventory: inventory:
usage: '/inventory <username> [version_uuid]' usage: '/<command> <username> [version_uuid]'
description: 'View & edit a player''s inventory' description: 'View & edit a player''s inventory'
enderchest: enderchest:
usage: '/enderchest <username> [version_uuid]' usage: '/<command> <username> [version_uuid]'
description: 'View & edit a player''s Ender Chest' description: 'View & edit a player''s Ender Chest'

@ -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&dagger; |
&dagger;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.

@ -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
<details>
<summary>Maven setup information</summary>
- Add the repository to your `pom.xml` as per below.
```xml
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
```
- 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
<dependency>
<groupId>net.william278</groupId>
<artifactId>HuskSync</artifactId>
<version>version</version>
<scope>provided</scope>
</dependency>
```
</details>
### 1.2 Setup with Gradle
<details>
<summary>Gradle setup information</summary>
- 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'
}
```
</details>
### 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]] &mdash; Get data snapshots and update current user data
- [[API Events]] &mdash; Listen to, cancel and modify the result of data synchronization events

@ -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 <migrator> [args]` | Migrate user data | _Console-only_ |
| `/userdata view <username> [version_uuid]` | View a snapshot of user data | `husksync.command.userdata` |
| `/userdata restore <username> <version_uuid>` | Restore a snapshot of user data | `husksync.command.userdata.manage` |
| `/userdata delete <username> <version_uuid>` | Delete a snapshot of user data | `husksync.command.userdata.manage` |
| `/userdata pin <username> <version_uuid>` | Pin a snapshot of user data | `husksync.command.userdata.manage` |
| `/inventory <username> [version_uuid]` | View a user's inventory contents | `husksync.command.inventory`&dagger; |
| `/enderchest <username> [version_uuid]` | View a user's ender chest contents | `husksync.command.enderchest`&dagger;|
&dagger; 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.

@ -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
<details>
<summary>config.yml</summary>
```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
```
</details>
## 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]].

@ -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 <username>`. 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 <username>`.
[![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.

@ -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 <user>` 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 <user> <snapshot-id> 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: `<username>_<timestamp>_<save-cause>_<short-uuid>.json`
<details>
<summary>Example output file: William278_2022-10-12_21-46-37_disconnect_f7719f5c.json</summary>
```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
}
```
</details>
### Dumping to the web
The Web Dump... button (equivalent to `/userdata dump <user> <snapshot-id> 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.

@ -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.

@ -0,0 +1,54 @@
This page addresses a number of frequently asked questions about the plugin.
## Frequently Asked Questions
<details>
<summary>&nbsp;<b>What data can be synchronised?</b></summary>
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.
</details>
<details>
<summary>&nbsp;<b>Is Redis required? What is Redis?</b></summary>
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)
</details>
<details>
<summary>&nbsp;<b>How does the plugin synchronise data?</b></summary>
[![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.
</details>
<details>
<summary>&nbsp;<b>Why doesn't HuskSync sync player economy balances / support Vault?</b></summary>
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&mdash;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.
</details>
<details>
<summary>&nbsp;<b>Is this better than MySQLPlayerDataBridge?</b></summary>
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.
</details>

@ -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)

@ -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**&mdash;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**&mdash;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`.
<details>
<summary>Example in config.yml</summary>
```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
```
</details>
## 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.

@ -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 <setting> <value>`.
- 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 <username>` 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.

@ -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 <setting> <value>`.
- 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 <username>` 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.

@ -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.

@ -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+) &mdash; 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]]

@ -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
&mdash;Supported&nbsp;&mdash;Unsupported&nbsp; ⚠️&mdash;Experimental
| Name | Description | Availability |
|---------------------------|-------------------------------------------------------------|:------------:|
| Inventories | Items in player inventories & selected hotbar slot | ✅ |
| Ender chests | Items in ender chests&midast; | ✅ |
| 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&dagger; | ✅ |
| 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)) | ❌ |
&midast;Purpur's custom ender chest resizing feature is also supported.
&dagger;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&mdash;that is, maps that have been locked in a cartography table&mdash;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.
<details>
<summary>Example in config.yml</summary>
```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
#...
```
</details>

@ -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!

@ -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.

@ -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<User>
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> 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<ItemData> 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());
}
}
});
}
}
```
<details>
<summary>HuskSyncAPI#getPlayerInventory()</summary>
```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());
}
}
});
}
```
</details>
<details>
<summary>HuskSyncAPI#getPlayerEnderChest()</summary>
```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());
}
}
});
}
```
</details>
## 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<StatusData> statusDataOptional = data.getStatus();
StatusData statusData = statusDataOptional.get();
statusData.health = 20;
// This returns a CompletableFuture<Void> that will invoke #thenRun() when it has completed
huskSyncAPI.setUserData(user, data).thenRun(() -> {
System.out.println("Healed " + user.username + "!");
});
}
});
}
}
```

@ -0,0 +1,2 @@
| This documentation is available via [william278.net](https://william278.net/docs/husksync/Home) |
| ----------------------------------------------------------------------------------------------- |

@ -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)

@ -3,8 +3,9 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=16 javaVersion=16
plugin_version=2.2.4 plugin_version=2.2.5
plugin_archive=husksync plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system
jedis_version=4.3.2 jedis_version=4.3.2
mysql_driver_version=8.0.32 mysql_driver_version=8.0.32

Loading…
Cancel
Save