forked from public-mirrors/HuskSync
1.0 release progress
parent
f842afac1e
commit
f7f1dc50eb
@ -1,20 +1,89 @@
|
|||||||
|
[![HuskSync Banner](images/banner-graphic.png)](https://github.com/WiIIiam278/HuskSync)
|
||||||
# HuskSync
|
# HuskSync
|
||||||
**HuskSync** is a robust solution for synchronising player data (inventories, health, hunger & status effects) between servers. It was designed as a much faster alternative to MySQLPlayerDataBridge,
|
[![Discord](https://img.shields.io/discord/818135932103557162?color=7289da&logo=discord)](https://discord.gg/tVYhJfyDWG)
|
||||||
|
|
||||||
## Installation
|
**HuskSync** is a modern, cross-server player data synchronisation system that allows player data (inventories, health, hunger & status effects) to be synchronised across servers through the use of **Redis**.
|
||||||
Install HuskSync in the `/plugins/` folder of your Spigot (and derivatives) servers and Proxy (BungeeCord and derivatives) server.
|
|
||||||
Start your servers, then stop them again to allow the configuration files to generate.
|
|
||||||
|
|
||||||
Navigate to the generated config.yml files on your Spigot server and Proxy (located in `/plugins/HuskSync/`) and fill in the credentials of your redis server. On the Proxy server, you can additionally configure a MySQL database to save player data in, as by default the plugin will create a SQLite database for this.
|
## Disclaimer
|
||||||
|
This source code is provided as reference to licensed individuals that have purchased the HuskSync plugin once from any of the official sources it is provided. The availability of this code does not grant you the rights to re-distribute, compile or share this source code outside this intended purpose.
|
||||||
|
|
||||||
If you have multiple proxy servers (i.e. via RedisBungee), you need to install the plugin on all of them and make use of the MySQL option and ensure the proxies are using the same database.
|
Are you a developer? [Read below for information about code bounty licensing](#Contributing).
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
### Requirements
|
||||||
|
* A BungeeCord-based proxy server
|
||||||
|
* A Spigot-based game server
|
||||||
|
* A Redis server
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Install HuskSync in the `/plugins/` folder of both your Spigot and Proxy servers.
|
||||||
|
2. Start your servers, then stop them again to allow the configuration files to generate.
|
||||||
|
3. Navigate to the generated `config.yml` files on your Spigot server and Proxy (located in `/plugins/HuskSync/`) and fill in the credentials of your redis server.
|
||||||
|
1. On the Proxy server, you can additionally configure a MySQL database to save player data in, as by default the plugin will create a SQLite database.
|
||||||
|
2. If you have multiple proxy servers (i.e. via RedisBungee), you need to install the plugin on all of them and make use of the MySQL option and ensure the proxies are using the same database.
|
||||||
|
3. By default, everything except player locations are synchronised. If you would like to change what gets synchronised, you can do this by editing the `config.yml` files of each Spigot server.
|
||||||
|
4. Once you have finished setting everything up, make sure to restart all of your servers and proxy server. Then, log in and data should be synchronised!
|
||||||
|
|
||||||
|
### Migration from MySQLPlayerDataBridge
|
||||||
|
HuskSync supports the migration of player data from [MySQLPlayerDataBridge](https://www.spigotmc.org/resources/mysql-player-data-bridge.8117/). Please note that HuskSync is not compatible with MySQLPlayerInventoryBridge, as that has a different system for data handling.
|
||||||
|
|
||||||
|
To migrate from MySQLPLayerDataBridge, you need a Proxy server with HuskSync installed and one Spigot server with both HuskSync and MySQLPlayerDataBridge installed. To migrate:
|
||||||
|
1. Make sure HuskSync is set up correctly on the Proxy and Spigot server, making sure that the two are able to communicate with Redis (it will display a handshake confirmation message in both consoles when communications have been established)
|
||||||
|
2. Make sure your database is configured correctly on your Proxy server. For example, if you would like to change from SQLite to MySQL, you should do this now because the data from MySQLPlayerDataBridge will be moved into it.
|
||||||
|
3. Make sure no players are online, then in the Proxy server's console run `husksync migrate`
|
||||||
|
4. Follow the steps in the Migration wizard to ensure the connection credentials and details of the database containing your MySQLPlayerDataBridge are correct, changing settings with `husksync migrate setting <setting> <new value>` as necessary.
|
||||||
|
5. Run `husksync migrate start` in the Proxy server's console to start the migration. This could take some time, depending on the amount of data that needs migrating and the speed of your database/server. When the migration is complete, it will display a "Migration complete" message.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
![Flow chart showing different processes of how the plugin works](images/flow-chart.png)
|
![Flow chart showing different processes of how the plugin works](images/flow-chart.png)
|
||||||
HuskSync synchronises player data between servers using Redis to transfer cached data, loaded from a central database as necessary.
|
HuskSync saves a player's data when they log out to a cache on your proxy server, and redistributes that data to players when they join another HuskSync-enabled server. Player data in the cache is then saved to a database (be it SQLite or MySQL) and this is loaded from when a player joins your network.
|
||||||
|
|
||||||
|
To facilitate the transfer of data between servers, HuskSync serializes player data and then makes use of Redis to communicate between the Proxy and Spigot servers.
|
||||||
|
|
||||||
|
### What is synchronised
|
||||||
|
Everything except player locations are synchronised by default. You can enable or disable what data is loaded on a server by modifying these values in the `/plugins/HuskSync/config.yml` file on each Spigot server.
|
||||||
|
* Player inventory
|
||||||
|
* Player armour and off-hand
|
||||||
|
* Player currently selected hotbar slot
|
||||||
|
* Player ender chest
|
||||||
|
* Player experience points & levels
|
||||||
|
* Player health
|
||||||
|
* Player max health
|
||||||
|
* Player health scale
|
||||||
|
* Player hunger
|
||||||
|
* Player saturation
|
||||||
|
* Player exhaustion
|
||||||
|
* Player game mode
|
||||||
|
* Player advancements
|
||||||
|
* Player statistics (ESC → Statistics menu)
|
||||||
|
* Player location
|
||||||
|
* Player flight status
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
Commands are handled by the proxy server, rather than each spigot server. Some will only work on Spigot servers with HuskSync installed. Please remember that you will need a Proxy permission plugin (e.g. LuckPermsBungee) to set permissions for proxy commands.
|
||||||
|
|
||||||
|
Command | Description | Permission
|
||||||
|
------- | ----------- | ----------
|
||||||
|
`/husksync about` | View plugin information | N/A
|
||||||
|
`/husksync status` | View system status information | `husksync.command.admin`
|
||||||
|
`/husksync reload` | Reload config & message files | `husksync.command.admin`
|
||||||
|
`/husksync invsee` | View an offline player's inventory | `husksync.command.inventory`
|
||||||
|
`/husksync echest` | View an offline player's ender chest | `husksync.command.ender_chest`
|
||||||
|
|
||||||
|
## Developers
|
||||||
|
### API
|
||||||
|
Coming soon!
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
A code bounty program is in place for HuskSync, where developers making significant code contributions to HuskSync may be entitled to a discretionary license to use HuskSync in commercial contexts without having to purchase the resource, so please feel free to submit pull requests with improvements, fixes and features!
|
||||||
|
|
||||||
|
### Building
|
||||||
|
To build HuskSync you will first need to download MySqlPlayerDataBridge and `mvn install:install-file` the jar file to your local maven repository.
|
||||||
|
```
|
||||||
|
mvn install:install-file -Dfile=MysqlPlayerDataBridge-v3.36.3.jar -DgroupId=net.craftersland.data -DartifactId=bridge -Dversion=3.36.3 -Dpackaging=jar
|
||||||
|
```
|
||||||
|
|
||||||
## Building
|
Then, to build the plugin, run the following in the root of the repository:
|
||||||
To build HuskSync, run the following in the root of the repository:
|
|
||||||
```
|
```
|
||||||
./gradlew clean build
|
./gradlew clean build
|
||||||
```
|
```
|
@ -0,0 +1,115 @@
|
|||||||
|
package me.william278.husksync.bukkit.data;
|
||||||
|
|
||||||
|
import me.william278.husksync.HuskSyncBukkit;
|
||||||
|
import me.william278.husksync.PlayerData;
|
||||||
|
import me.william278.husksync.Settings;
|
||||||
|
import me.william278.husksync.bukkit.PlayerSetter;
|
||||||
|
import me.william278.husksync.redis.RedisMessage;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used for managing viewing inventories using inventory-see command
|
||||||
|
*/
|
||||||
|
public class DataViewer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a viewer's data to a viewer
|
||||||
|
*
|
||||||
|
* @param viewer The viewing {@link Player} who will see the data
|
||||||
|
* @param data The {@link DataView} to show the viewer
|
||||||
|
* @throws IOException If an exception occurred deserializing item data
|
||||||
|
*/
|
||||||
|
public static void showData(Player viewer, DataView data) throws IOException {
|
||||||
|
// Show an inventory with the viewer's inventory and equipment
|
||||||
|
viewer.closeInventory();
|
||||||
|
viewer.openInventory(createInventory(viewer, data));
|
||||||
|
|
||||||
|
// Set the viewer as viewing
|
||||||
|
HuskSyncBukkit.bukkitCache.setViewing(viewer.getUniqueId(), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles what happens after a data viewer finishes viewing data
|
||||||
|
*
|
||||||
|
* @param viewer The viewing {@link Player} who was looking at data
|
||||||
|
* @param inventory The {@link Inventory} that was being viewed
|
||||||
|
* @throws IOException If an exception occurred serializing item data
|
||||||
|
*/
|
||||||
|
public static void stopShowing(Player viewer, Inventory inventory) throws IOException {
|
||||||
|
// Get the DataView the player was looking at
|
||||||
|
DataView dataView = HuskSyncBukkit.bukkitCache.getViewing(viewer.getUniqueId());
|
||||||
|
|
||||||
|
// Set the player as no longer viewing an inventory
|
||||||
|
HuskSyncBukkit.bukkitCache.removeViewing(viewer.getUniqueId());
|
||||||
|
|
||||||
|
// Get and update the PlayerData with the new item data
|
||||||
|
PlayerData playerData = dataView.playerData();
|
||||||
|
String serializedItemData = DataSerializer.itemStackArrayToBase64(inventory.getContents());
|
||||||
|
switch (dataView.inventoryType()) {
|
||||||
|
case INVENTORY -> playerData.setSerializedInventory(serializedItemData);
|
||||||
|
case ENDER_CHEST -> playerData.setSerializedEnderChest(serializedItemData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a redis message with the updated data after the viewing
|
||||||
|
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||||
|
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
|
||||||
|
RedisMessage.serialize(playerData))
|
||||||
|
.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the inventory object that the viewer will see
|
||||||
|
*
|
||||||
|
* @param viewer The {@link Player} who will view the data
|
||||||
|
* @param data The {@link DataView} data to view
|
||||||
|
* @return The {@link Inventory} that the viewer will see
|
||||||
|
* @throws IOException If an exception occurred deserializing item data
|
||||||
|
*/
|
||||||
|
private static Inventory createInventory(Player viewer, DataView data) throws IOException {
|
||||||
|
Inventory inventory = switch (data.inventoryType) {
|
||||||
|
case INVENTORY -> Bukkit.createInventory(viewer, 45, data.ownerName + "'s Inventory");
|
||||||
|
case ENDER_CHEST -> Bukkit.createInventory(viewer, 27, data.ownerName + "'s Ender Chest");
|
||||||
|
};
|
||||||
|
PlayerSetter.setInventory(inventory, data.getDeserializedData());
|
||||||
|
return inventory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents Player Data being viewed by a {@link Player}
|
||||||
|
*/
|
||||||
|
public record DataView(PlayerData playerData, String ownerName, InventoryType inventoryType) {
|
||||||
|
/**
|
||||||
|
* What kind of item data is being viewed
|
||||||
|
*/
|
||||||
|
public enum InventoryType {
|
||||||
|
/**
|
||||||
|
* A player's inventory
|
||||||
|
*/
|
||||||
|
INVENTORY,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A player's ender chest
|
||||||
|
*/
|
||||||
|
ENDER_CHEST
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the deserialized data currently being viewed
|
||||||
|
*
|
||||||
|
* @return The deserialized item data, as an {@link ItemStack[]} array
|
||||||
|
* @throws IOException If an exception occurred deserializing item data
|
||||||
|
*/
|
||||||
|
public ItemStack[] getDeserializedData() throws IOException {
|
||||||
|
return switch (inventoryType) {
|
||||||
|
case INVENTORY -> DataSerializer.itemStackArrayFromBase64(playerData.getSerializedInventory());
|
||||||
|
case ENDER_CHEST -> DataSerializer.itemStackArrayFromBase64(playerData.getSerializedEnderChest());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,81 @@
|
|||||||
|
package me.william278.husksync.bukkit.migrator;
|
||||||
|
|
||||||
|
import me.william278.husksync.HuskSyncBukkit;
|
||||||
|
import me.william278.husksync.PlayerData;
|
||||||
|
import me.william278.husksync.bukkit.PlayerSetter;
|
||||||
|
import me.william278.husksync.bukkit.data.DataSerializer;
|
||||||
|
import me.william278.husksync.migrator.MPDBPlayerData;
|
||||||
|
import net.craftersland.data.bridge.PD;
|
||||||
|
import org.bukkit.Bukkit;
|
||||||
|
import org.bukkit.event.inventory.InventoryType;
|
||||||
|
import org.bukkit.inventory.Inventory;
|
||||||
|
import org.bukkit.inventory.ItemStack;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
public class MPDBDeserializer {
|
||||||
|
|
||||||
|
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||||
|
|
||||||
|
// Instance of MySqlPlayerDataBridge
|
||||||
|
private static PD mySqlPlayerDataBridge;
|
||||||
|
public static void setMySqlPlayerDataBridge() {
|
||||||
|
mySqlPlayerDataBridge = (PD) Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert MySqlPlayerDataBridge ({@link MPDBPlayerData}) data to HuskSync's {@link PlayerData}
|
||||||
|
*
|
||||||
|
* @param mpdbPlayerData The {@link MPDBPlayerData} to convert
|
||||||
|
* @return The converted {@link PlayerData}
|
||||||
|
*/
|
||||||
|
public static PlayerData convertMPDBData(MPDBPlayerData mpdbPlayerData) {
|
||||||
|
PlayerData playerData = PlayerData.DEFAULT_PLAYER_DATA(mpdbPlayerData.playerUUID);
|
||||||
|
playerData.useDefaultData = false;
|
||||||
|
if (!HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "MySqlPlayerDataBridge is not installed, failed to serialize data!");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the data
|
||||||
|
try {
|
||||||
|
// Set inventory
|
||||||
|
Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||||
|
PlayerSetter.setInventory(inventory, getItemStackArrayFromMPDBBase64String(mpdbPlayerData.inventoryData));
|
||||||
|
|
||||||
|
playerData.setSerializedInventory(DataSerializer.getSerializedInventoryContents(inventory));
|
||||||
|
inventory.clear();
|
||||||
|
|
||||||
|
// Set ender chest
|
||||||
|
playerData.setSerializedEnderChest(DataSerializer.itemStackArrayToBase64(
|
||||||
|
getItemStackArrayFromMPDBBase64String(mpdbPlayerData.enderChestData)));
|
||||||
|
|
||||||
|
// Set experience
|
||||||
|
playerData.setExpLevel(mpdbPlayerData.expLevel);
|
||||||
|
playerData.setExpProgress(mpdbPlayerData.expProgress);
|
||||||
|
playerData.setTotalExperience(mpdbPlayerData.totalExperience);
|
||||||
|
} catch (IOException | InvocationTargetException | IllegalAccessException e) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to convert MPDB data to HuskSync's format!");
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return playerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an ItemStack array from a decoded base 64 string in MySQLPlayerDataBridge's format
|
||||||
|
*
|
||||||
|
* @param data The encoded ItemStack[] string from MySQLPlayerDataBridge
|
||||||
|
* @return The {@link ItemStack[]} array
|
||||||
|
* @throws IOException If an error occurs during decoding
|
||||||
|
* @throws InvocationTargetException If an error occurs during decoding
|
||||||
|
* @throws IllegalAccessException If an error occurs during decoding
|
||||||
|
*/
|
||||||
|
public static ItemStack[] getItemStackArrayFromMPDBBase64String(String data) throws IOException, InvocationTargetException, IllegalAccessException {
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
return new ItemStack[0];
|
||||||
|
}
|
||||||
|
return mySqlPlayerDataBridge.getItemStackSerializer().fromBase64(data);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,278 @@
|
|||||||
|
package me.william278.husksync.bungeecord.migrator;
|
||||||
|
|
||||||
|
import me.william278.husksync.HuskSyncBungeeCord;
|
||||||
|
import me.william278.husksync.PlayerData;
|
||||||
|
import me.william278.husksync.Settings;
|
||||||
|
import me.william278.husksync.bungeecord.data.DataManager;
|
||||||
|
import me.william278.husksync.bungeecord.data.sql.Database;
|
||||||
|
import me.william278.husksync.bungeecord.data.sql.MySQL;
|
||||||
|
import me.william278.husksync.migrator.MPDBPlayerData;
|
||||||
|
import me.william278.husksync.redis.RedisMessage;
|
||||||
|
import net.md_5.bungee.api.ProxyServer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle migration of data from MySQLPlayerDataBridge
|
||||||
|
* <p>
|
||||||
|
* The migrator accesses and decodes MPDB's format directly.
|
||||||
|
* It does this by establishing a connection
|
||||||
|
*/
|
||||||
|
public class MPDBMigrator {
|
||||||
|
|
||||||
|
public static int migratedDataSent = 0;
|
||||||
|
public static int playersMigrated = 0;
|
||||||
|
|
||||||
|
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||||
|
|
||||||
|
public static HashMap<PlayerData,String> incomingPlayerData;
|
||||||
|
|
||||||
|
public static MigrationSettings migrationSettings = new MigrationSettings();
|
||||||
|
private static Database sourceDatabase;
|
||||||
|
|
||||||
|
private static HashSet<MPDBPlayerData> mpdbPlayerData;
|
||||||
|
|
||||||
|
public void start() {
|
||||||
|
if (ProxyServer.getInstance().getPlayers().size() > 0) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to start migration because there are players online. " +
|
||||||
|
"Your network has to be empty to migrate data for safety reasons.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int synchronisedServersWithMpdb = 0;
|
||||||
|
for (HuskSyncBungeeCord.Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||||
|
if (server.hasMySqlPlayerDataBridge()) {
|
||||||
|
synchronisedServersWithMpdb++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (synchronisedServersWithMpdb < 1) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to start migration because at least one Spigot server must be online and have both HuskSync and MySqlPlayerDataBridge installed. " +
|
||||||
|
"Please start one Spigot server with HuskSync installed to begin migration.");
|
||||||
|
}
|
||||||
|
|
||||||
|
migratedDataSent = 0;
|
||||||
|
playersMigrated = 0;
|
||||||
|
mpdbPlayerData = new HashSet<>();
|
||||||
|
incomingPlayerData = new HashMap<>();
|
||||||
|
final MigrationSettings settings = migrationSettings;
|
||||||
|
|
||||||
|
// Get connection to source database
|
||||||
|
sourceDatabase = new MigratorMySQL(plugin, settings.sourceHost, settings.sourcePort,
|
||||||
|
settings.sourceDatabase, settings.sourceUsername, settings.sourcePassword);
|
||||||
|
sourceDatabase.load();
|
||||||
|
if (sourceDatabase.isInactive()) {
|
||||||
|
plugin.getLogger().log(Level.WARNING, "Failed to establish connection to the origin MySQL database. " +
|
||||||
|
"Please check you have input the correct connection details and try again.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||||
|
prepareTargetDatabase();
|
||||||
|
|
||||||
|
getInventoryData();
|
||||||
|
|
||||||
|
getEnderChestData();
|
||||||
|
|
||||||
|
getExperienceData();
|
||||||
|
|
||||||
|
sendEncodedData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the new database out of current data
|
||||||
|
private void prepareTargetDatabase() {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Preparing target database...");
|
||||||
|
try (Connection connection = HuskSyncBungeeCord.getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + Database.PLAYER_TABLE_NAME + ";")) {
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + Database.DATA_TABLE_NAME + ";")) {
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "An exception occurred preparing the target database", e);
|
||||||
|
} finally {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Finished preparing target database!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getInventoryData() {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Getting inventory data from MySQLPlayerDataBridge...");
|
||||||
|
try (Connection connection = sourceDatabase.getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.inventoryDataTable + ";")) {
|
||||||
|
ResultSet resultSet = statement.executeQuery();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
|
||||||
|
final String playerName = resultSet.getString("player_name");
|
||||||
|
|
||||||
|
MPDBPlayerData data = new MPDBPlayerData(playerUUID, playerName);
|
||||||
|
data.inventoryData = resultSet.getString("inventory");
|
||||||
|
data.armorData = resultSet.getString("armor");
|
||||||
|
|
||||||
|
mpdbPlayerData.add(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "An exception occurred getting inventory data", e);
|
||||||
|
} finally {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Finished getting inventory data from MySQLPlayerDataBridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getEnderChestData() {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Getting ender chest data from MySQLPlayerDataBridge...");
|
||||||
|
try (Connection connection = sourceDatabase.getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.enderChestDataTable + ";")) {
|
||||||
|
ResultSet resultSet = statement.executeQuery();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
|
||||||
|
|
||||||
|
for (MPDBPlayerData data : mpdbPlayerData) {
|
||||||
|
if (data.playerUUID.equals(playerUUID)) {
|
||||||
|
data.enderChestData = resultSet.getString("enderchest");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "An exception occurred getting ender chest", e);
|
||||||
|
} finally {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Finished getting ender chest data from MySQLPlayerDataBridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void getExperienceData() {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Getting experience data from MySQLPlayerDataBridge...");
|
||||||
|
try (Connection connection = sourceDatabase.getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.expDataTable + ";")) {
|
||||||
|
ResultSet resultSet = statement.executeQuery();
|
||||||
|
while (resultSet.next()) {
|
||||||
|
final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
|
||||||
|
|
||||||
|
for (MPDBPlayerData data : mpdbPlayerData) {
|
||||||
|
if (data.playerUUID.equals(playerUUID)) {
|
||||||
|
data.expLevel = resultSet.getInt("exp_lvl");
|
||||||
|
data.expProgress = resultSet.getInt("exp");
|
||||||
|
data.totalExperience = resultSet.getInt("total_exp");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "An exception occurred getting ender chest", e);
|
||||||
|
} finally {
|
||||||
|
plugin.getLogger().log(Level.INFO, "Finished getting experience data from MySQLPlayerDataBridge");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendEncodedData() {
|
||||||
|
for (HuskSyncBungeeCord.Server processingServer : HuskSyncBungeeCord.synchronisedServers) {
|
||||||
|
if (processingServer.hasMySqlPlayerDataBridge()) {
|
||||||
|
for (MPDBPlayerData data : mpdbPlayerData) {
|
||||||
|
try {
|
||||||
|
new RedisMessage(RedisMessage.MessageType.DECODE_MPDB_DATA,
|
||||||
|
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null),
|
||||||
|
processingServer.serverUUID().toString(),
|
||||||
|
RedisMessage.serialize(data))
|
||||||
|
.send();
|
||||||
|
migratedDataSent++;
|
||||||
|
} catch (IOException e) {
|
||||||
|
plugin.getLogger().log(Level.SEVERE, "Failed to serialize encoded MPDB data", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.getLogger().log(Level.INFO, "Finished dispatching encoded data for " + migratedDataSent + " players; please wait for conversion to finish");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all incoming decoded MPDB data to cache / SQL
|
||||||
|
*/
|
||||||
|
public static void loadIncomingData() {
|
||||||
|
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||||
|
int playersSaved = 0;
|
||||||
|
plugin.getLogger().log(Level.INFO, "Saving data for " + playersMigrated + " players...");
|
||||||
|
|
||||||
|
for (PlayerData playerData : incomingPlayerData.keySet()) {
|
||||||
|
String playerName = incomingPlayerData.get(playerData);
|
||||||
|
|
||||||
|
// Add the player to the MySQL table
|
||||||
|
DataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName);
|
||||||
|
|
||||||
|
// Update the data in the cache and SQL
|
||||||
|
DataManager.updatePlayerData(playerData);
|
||||||
|
|
||||||
|
playersSaved++;
|
||||||
|
plugin.getLogger().log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as done when done
|
||||||
|
plugin.getLogger().log(Level.INFO, """
|
||||||
|
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||||
|
|
||||||
|
Migration complete!
|
||||||
|
|
||||||
|
Successfully migrated data for %1%/%2% players.
|
||||||
|
|
||||||
|
You should now uninstall MySQLPlayerDataBridge from
|
||||||
|
the rest of the Spigot servers, then restart them.
|
||||||
|
""".replaceAll("%1%", Integer.toString(MPDBMigrator.playersMigrated))
|
||||||
|
.replaceAll("%2%", Integer.toString(MPDBMigrator.migratedDataSent)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to hold settings for the MPDB migration
|
||||||
|
*/
|
||||||
|
public static class MigrationSettings {
|
||||||
|
public String sourceHost;
|
||||||
|
public int sourcePort;
|
||||||
|
public String sourceDatabase;
|
||||||
|
public String sourceUsername;
|
||||||
|
public String sourcePassword;
|
||||||
|
|
||||||
|
public String inventoryDataTable;
|
||||||
|
public String enderChestDataTable;
|
||||||
|
public String expDataTable;
|
||||||
|
|
||||||
|
public MigrationSettings() {
|
||||||
|
sourceHost = "localhost";
|
||||||
|
sourcePort = 3306;
|
||||||
|
sourceDatabase = "mpdb";
|
||||||
|
sourceUsername = "root";
|
||||||
|
sourcePassword = "pa55w0rd";
|
||||||
|
|
||||||
|
inventoryDataTable = "mpdb_inventory";
|
||||||
|
enderChestDataTable = "mpdb_enderchest";
|
||||||
|
expDataTable = "mpdb_experience";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL class used for importing data from MPDB
|
||||||
|
*/
|
||||||
|
public static class MigratorMySQL extends MySQL {
|
||||||
|
public MigratorMySQL(HuskSyncBungeeCord instance, String host, int port, String database, String username, String password) {
|
||||||
|
super(instance);
|
||||||
|
super.host = host;
|
||||||
|
super.port = port;
|
||||||
|
super.database = database;
|
||||||
|
super.username = username;
|
||||||
|
super.password = password;
|
||||||
|
super.params = "?useSSL=false";
|
||||||
|
super.dataPoolName = DATA_POOL_NAME + "Migrator";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package me.william278.husksync;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
|
public class MessageManager {
|
||||||
|
|
||||||
|
private static HashMap<String, String> messages = new HashMap<>();
|
||||||
|
|
||||||
|
public static void setMessages(HashMap<String, String> newMessages) {
|
||||||
|
messages = new HashMap<>(newMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getMessage(String messageId) {
|
||||||
|
return messages.get(messageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[HuskSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% (%bukkit_brand% v%bukkit_version%)](#00fb9a)\n")
|
||||||
|
.append("[%plugin_description%](gray)\n")
|
||||||
|
.append("[• Author:](white) [William278](gray show_text=&7Click to pay a visit open_url=https://youtube.com/William27528)\n")
|
||||||
|
.append("[• Help Wiki:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/wiki/)\n")
|
||||||
|
.append("[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)\n")
|
||||||
|
.append("[• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)");
|
||||||
|
|
||||||
|
public static StringBuilder PLUGIN_STATUS = new StringBuilder().append("[HuskSync](#00fb9a bold) [| Current system status:](#00fb9a)\n")
|
||||||
|
.append("[• Connected servers:](white) [%1%](#00fb9a)")
|
||||||
|
.append("[• Cached player data:](white) [%2%](#00fb9a)");
|
||||||
|
|
||||||
|
}
|
@ -1,16 +0,0 @@
|
|||||||
package me.william278.husksync;
|
|
||||||
|
|
||||||
public class MessageStrings {
|
|
||||||
|
|
||||||
public static final StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[HuskSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% (%bukkit_brand% v%bukkit_version%)](#00fb9a)\n")
|
|
||||||
.append("[%plugin_description%](gray)\n")
|
|
||||||
.append("[• Author:](white) [William278](gray show_text=&7Click to pay a visit open_url=https://youtube.com/William27528)\n")
|
|
||||||
.append("[• Help Wiki:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/wiki/)\n")
|
|
||||||
.append("[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)\n")
|
|
||||||
.append("[• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)");
|
|
||||||
|
|
||||||
public static final String ERROR_INVALID_SYNTAX = "[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)";
|
|
||||||
|
|
||||||
public static final String SYNCHRONISATION_COMPLETE = "[Data synchronised!](#00fb9a)";
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,35 @@
|
|||||||
|
package me.william278.husksync.migrator;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class that stores player data taken from MPDB's database, that can then be converted into HuskSync's format
|
||||||
|
*/
|
||||||
|
public class MPDBPlayerData implements Serializable {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Player information
|
||||||
|
*/
|
||||||
|
public final UUID playerUUID;
|
||||||
|
public final String playerName;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Inventory, ender chest and armor data
|
||||||
|
*/
|
||||||
|
public String inventoryData;
|
||||||
|
public String armorData;
|
||||||
|
public String enderChestData;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Experience data
|
||||||
|
*/
|
||||||
|
public int expLevel;
|
||||||
|
public float expProgress;
|
||||||
|
public int totalExperience;
|
||||||
|
|
||||||
|
public MPDBPlayerData(UUID playerUUID, String playerName) {
|
||||||
|
this.playerUUID = playerUUID;
|
||||||
|
this.playerName = playerName;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
synchronisation_complete: '[Data synchronised!](#00fb9a)'
|
||||||
|
viewing_inventory_of: '[Viewing the inventory of](#00fb9a) [%1%](#00fb9a bold)'
|
||||||
|
viewing_ender_chest_of: '[Viewing the ender chest of](#00fb9a) [%1%](#00fb9a bold)'
|
||||||
|
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)'
|
||||||
|
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)'
|
||||||
|
error_invalid_player: '[Error:](#ff3300) [Could not find that player](#ff7e5e)'
|
||||||
|
error_no_permission: '[Error:](#ff3300) [You do not have permission to execute this command](#ff7e5e)'
|
||||||
|
error_cannot_view_inventory_online: '[Error:](#ff3300) [You can''t access the inventory of an online player through HuskSync](#ff7e5e)'
|
||||||
|
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [You can''t access the ender chest of an online player through HuskSync](#ff7e5e)'
|
||||||
|
error_cannot_view_own_inventory: '[Error:](#ff3300) [You can''t access your own inventory!](#ff7e5e)'
|
||||||
|
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [You can''t access your own ender chest!](#ff7e5e)'
|
||||||
|
error_console_command_only: '[Error:](#ff3300) [That command can only be run through the %1% console](#ff7e5e)'
|
||||||
|
error_no_servers_proxied: '[Error:](#ff3300) [Failed to process operation; no servers are online that have HuskSync installed.\nPlease ensure HuskSync is installed on both the Proxy server and all servers you wish to synchronise data between](#ff7e5e)'
|
@ -1,6 +1,7 @@
|
|||||||
name: CrossServerSync
|
name: HuskSync
|
||||||
version: @version@
|
version: @version@
|
||||||
main: me.william278.husksync.HuskSyncBukkit
|
main: me.william278.husksync.HuskSyncBukkit
|
||||||
api-version: 1.16
|
api-version: 1.16
|
||||||
author: William278
|
author: William278
|
||||||
description: 'Synchronize data cross-server'
|
description: 'A modern, cross-server player data synchronisation system'
|
||||||
|
softdepend: [MysqlPlayerDataBridge]
|
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 159 KiB |
Loading…
Reference in New Issue