mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-19 14:59:21 +00:00
Add information command
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# CrossServerSync
|
||||
**CrossServerSync** is a robust solution for synchronising player data (inventories, health, hunger & status effects) between servers. It was designed as a lightweight alternative to MySQLPlayerDataBridge,
|
||||
**CrossServerSync** 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,
|
||||
|
||||
### Installation
|
||||
Install CrossServerSync in the `/plugins/` folder of your Spigot (and derivatives) servers and Proxy (BungeeCord and derivatives) server.
|
||||
|
||||
@@ -4,6 +4,7 @@ dependencies {
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:2.2.1'
|
||||
implementation 'redis.clients:jedis:3.7.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
}
|
||||
@@ -11,6 +12,7 @@ dependencies {
|
||||
shadowJar {
|
||||
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
|
||||
relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan'
|
||||
relocate 'de.themoep', 'me.William278.crossserversync.libraries.minedown'
|
||||
}
|
||||
|
||||
tasks.register('prepareKotlinBuildScriptModel'){}
|
||||
@@ -27,7 +27,9 @@ public class PlayerSetter {
|
||||
player.setMaxHealth(data.getMaxHealth());
|
||||
player.setFoodLevel(data.getHunger());
|
||||
player.setSaturation(data.getSaturation());
|
||||
player.setExhaustion(data.getSaturationExhaustion());
|
||||
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
|
||||
|
||||
//todo potion effects not working
|
||||
setPlayerPotionEffects(player, DataSerializer.potionEffectArrayFromBase64(data.getSerializedEffectData()));
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package me.william278.crossserversync.bukkit.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import me.william278.crossserversync.MessageStrings;
|
||||
import me.william278.crossserversync.PlayerData;
|
||||
import me.william278.crossserversync.Settings;
|
||||
import me.william278.crossserversync.CrossServerSyncBukkit;
|
||||
@@ -35,7 +37,8 @@ public class BukkitRedisListener extends RedisListener {
|
||||
// Handle the message for the player
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
|
||||
if (message.getMessageType().equals(RedisMessage.MessageType.PLAYER_DATA_REPLY)) {
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_SET -> {
|
||||
try {
|
||||
// Deserialize the received PlayerData
|
||||
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
|
||||
@@ -50,6 +53,19 @@ public class BukkitRedisListener extends RedisListener {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case SEND_PLUGIN_INFORMATION -> {
|
||||
String proxyBrand = message.getMessageDataElements()[0];
|
||||
String proxyVersion = message.getMessageDataElements()[1];
|
||||
assert plugin.getDescription().getDescription() != null;
|
||||
player.spigot().sendMessage(new MineDown(MessageStrings.PLUGIN_INFORMATION.toString()
|
||||
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
|
||||
.replaceAll("%proxy_brand%", proxyBrand)
|
||||
.replaceAll("%proxy_version%", proxyVersion)
|
||||
.replaceAll("%bukkit_brand%", Bukkit.getName())
|
||||
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
|
||||
.toComponent());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public class EventListener implements Listener {
|
||||
player.getMaxHealth(),
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
DataSerializer.getSerializedEffectData(player)));
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ dependencies {
|
||||
|
||||
implementation 'redis.clients:jedis:3.7.0'
|
||||
implementation 'com.zaxxer:HikariCP:5.0.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
|
||||
compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT'
|
||||
}
|
||||
@@ -12,6 +13,7 @@ shadowJar {
|
||||
relocate 'redis.clients', 'me.William278.crossserversync.libraries.jedis'
|
||||
relocate 'com.zaxxer', 'me.William278.crossserversync.libraries.hikari'
|
||||
relocate 'org.bstats', 'me.William278.crossserversync.libraries.plan'
|
||||
relocate 'de.themoep', 'me.William278.crossserversync.libraries.minedown'
|
||||
}
|
||||
|
||||
tasks.register('prepareKotlinBuildScriptModel'){}
|
||||
@@ -1,5 +1,6 @@
|
||||
package me.william278.crossserversync;
|
||||
|
||||
import me.william278.crossserversync.bungeecord.command.CrossServerSyncCommand;
|
||||
import me.william278.crossserversync.bungeecord.config.ConfigLoader;
|
||||
import me.william278.crossserversync.bungeecord.config.ConfigManager;
|
||||
import me.william278.crossserversync.bungeecord.data.DataManager;
|
||||
@@ -51,9 +52,12 @@ public final class CrossServerSyncBungeeCord extends Plugin {
|
||||
// Setup player data cache
|
||||
DataManager.playerDataCache = new DataManager.PlayerDataCache();
|
||||
|
||||
// Initialize PreLoginEvent listener
|
||||
// Register listener
|
||||
getProxy().getPluginManager().registerListener(this, new BungeeEventListener());
|
||||
|
||||
// Register command
|
||||
getProxy().getPluginManager().registerCommand(this, new CrossServerSyncCommand());
|
||||
|
||||
// Initialize the redis listener
|
||||
new BungeeRedisListener();
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package me.william278.crossserversync.bungeecord.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import me.william278.crossserversync.CrossServerSyncBungeeCord;
|
||||
import me.william278.crossserversync.MessageStrings;
|
||||
import me.william278.crossserversync.Settings;
|
||||
import me.william278.crossserversync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.plugin.Command;
|
||||
import net.md_5.bungee.api.plugin.TabExecutor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class CrossServerSyncCommand extends Command implements TabExecutor {
|
||||
|
||||
private final static CrossServerSyncBungeeCord plugin = CrossServerSyncBungeeCord.getInstance();
|
||||
private final static String[] COMMAND_TAB_ARGUMENTS = {"about", "reload"};
|
||||
private final static String PERMISSION = "crossserversync.command.csc";
|
||||
|
||||
public CrossServerSyncCommand() { super("csc", PERMISSION, "crossserversync"); }
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (args.length == 1) {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "about", "info" -> sendAboutInformation(player);
|
||||
|
||||
default -> sender.sendMessage(new MineDown(MessageStrings.ERROR_INVALID_SYNTAX.replaceAll("%1%", "/csc <about>")).toComponent());
|
||||
}
|
||||
} else {
|
||||
sendAboutInformation(player);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send information about the plugin
|
||||
* @param player The player to send it to
|
||||
*/
|
||||
private void sendAboutInformation(ProxiedPlayer player) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId()),
|
||||
plugin.getProxy().getName(), plugin.getDescription().getVersion()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (!player.hasPermission(PERMISSION)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (args.length == 1) {
|
||||
return Arrays.stream(COMMAND_TAB_ARGUMENTS).filter(val -> val.startsWith(args[0]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -73,12 +73,13 @@ public class DataManager {
|
||||
final double maxHealth = resultSet.getDouble("max_health");
|
||||
final int hunger = resultSet.getInt("hunger");
|
||||
final float saturation = resultSet.getFloat("saturation");
|
||||
final float saturationExhaustion = resultSet.getFloat("saturation_exhaustion");
|
||||
final int selectedSlot = resultSet.getInt("selected_slot");
|
||||
final String serializedStatusEffects = resultSet.getString("status_effects");
|
||||
|
||||
return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, selectedSlot, serializedStatusEffects);
|
||||
return new PlayerData(playerUUID, dataVersionUUID, serializedInventory, serializedEnderChest, health, maxHealth, hunger, saturation, saturationExhaustion, selectedSlot, serializedStatusEffects);
|
||||
} else {
|
||||
return PlayerData.EMPTY_PLAYER_DATA(playerUUID);
|
||||
return PlayerData.DEFAULT_PLAYER_DATA(playerUUID);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -104,7 +105,7 @@ public class DataManager {
|
||||
private static void updatePlayerSQLData(PlayerData playerData) {
|
||||
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `hunger`=?, `saturation`=?, `selected_slot`=?, `status_effects`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) {
|
||||
"UPDATE " + Database.DATA_TABLE_NAME + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `hunger`=?, `saturation`=?, `saturation_exhaustion`=?, `selected_slot`=?, `status_effects`=? WHERE `player_id`=(SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?);")) {
|
||||
statement.setString(1, playerData.getDataVersionUUID().toString());
|
||||
statement.setTimestamp(2, new Timestamp(Instant.now().getEpochSecond()));
|
||||
statement.setString(3, playerData.getSerializedInventory());
|
||||
@@ -113,9 +114,10 @@ public class DataManager {
|
||||
statement.setDouble(6, playerData.getMaxHealth()); // Max health
|
||||
statement.setInt(7, playerData.getHunger()); // Hunger
|
||||
statement.setFloat(8, playerData.getSaturation()); // Saturation
|
||||
statement.setInt(9, playerData.getSelectedSlot());
|
||||
statement.setString(10, playerData.getSerializedEffectData()); // Status effects
|
||||
statement.setString(11, playerData.getPlayerUUID().toString());
|
||||
statement.setFloat(9, playerData.getSaturationExhaustion()); // Saturation exhaustion
|
||||
statement.setInt(10, playerData.getSelectedSlot()); // Current selected slot
|
||||
statement.setString(11, playerData.getSerializedEffectData()); // Status effects
|
||||
statement.setString(12, playerData.getPlayerUUID().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
@@ -126,7 +128,7 @@ public class DataManager {
|
||||
private static void insertPlayerData(PlayerData playerData) {
|
||||
try (Connection connection = CrossServerSyncBungeeCord.getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`hunger`,`saturation`,`selected_slot`,`status_effects`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?);")) {
|
||||
"INSERT INTO " + Database.DATA_TABLE_NAME + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`hunger`,`saturation`,`saturation_exhaustion`,`selected_slot`,`status_effects`) VALUES((SELECT `id` FROM " + Database.PLAYER_TABLE_NAME + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?);")) {
|
||||
statement.setString(1, playerData.getPlayerUUID().toString());
|
||||
statement.setString(2, playerData.getDataVersionUUID().toString());
|
||||
statement.setTimestamp(3, new Timestamp(Instant.now().getEpochSecond()));
|
||||
@@ -136,8 +138,9 @@ public class DataManager {
|
||||
statement.setDouble(7, playerData.getMaxHealth()); // Max health
|
||||
statement.setInt(8, playerData.getHunger()); // Hunger
|
||||
statement.setFloat(9, playerData.getSaturation()); // Saturation
|
||||
statement.setInt(10, playerData.getSelectedSlot());
|
||||
statement.setString(11, playerData.getSerializedEffectData()); // Status effects
|
||||
statement.setFloat(10, playerData.getSaturationExhaustion()); // Saturation exhaustion
|
||||
statement.setInt(11, playerData.getSelectedSlot()); // Current selected slot
|
||||
statement.setString(12, playerData.getSerializedEffectData()); // Status effects
|
||||
|
||||
statement.executeUpdate();
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ public class MySQL extends Database {
|
||||
"`max_health` double NOT NULL," +
|
||||
"`hunger` integer NOT NULL," +
|
||||
"`saturation` float NOT NULL," +
|
||||
"`saturation_exhaustion` float NOT NULL," +
|
||||
"`selected_slot` integer NOT NULL," +
|
||||
"`status_effects` longtext NOT NULL," +
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ public class SQLite extends Database {
|
||||
"`max_health` double NOT NULL," +
|
||||
"`hunger` integer NOT NULL," +
|
||||
"`saturation` float NOT NULL," +
|
||||
"`saturation_exhaustion` float NOT NULL," +
|
||||
"`selected_slot` integer NOT NULL," +
|
||||
"`status_effects` longtext NOT NULL," +
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ public class BungeeRedisListener extends RedisListener {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
try {
|
||||
// Send the reply, serializing the message data
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REPLY,
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID),
|
||||
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID))).send();
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package me.william278.crossserversync;
|
||||
|
||||
public class MessageStrings {
|
||||
|
||||
public static final StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[CrossServerSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% | %bukkit_brand% Version %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/CrossServerSync/wiki/)\n")
|
||||
.append("[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/CrossServerSync/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)";
|
||||
|
||||
}
|
||||
@@ -23,10 +23,10 @@ public class PlayerData implements Serializable {
|
||||
private final double maxHealth;
|
||||
private final int hunger;
|
||||
private final float saturation;
|
||||
private final float saturationExhaustion;
|
||||
private final int selectedSlot;
|
||||
private final String serializedEffectData;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new PlayerData object; a random data version UUID will be selected.
|
||||
* @param playerUUID UUID of the player
|
||||
@@ -39,7 +39,7 @@ public class PlayerData implements Serializable {
|
||||
* @param selectedSlot Player selected slot
|
||||
* @param serializedStatusEffects Serialized status effect data
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, int selectedSlot, String serializedStatusEffects) {
|
||||
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, float saturationExhaustion, int selectedSlot, String serializedStatusEffects) {
|
||||
this.dataVersionUUID = UUID.randomUUID();
|
||||
this.playerUUID = playerUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
@@ -48,11 +48,12 @@ public class PlayerData implements Serializable {
|
||||
this.maxHealth = maxHealth;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
}
|
||||
|
||||
public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, int selectedSlot, String serializedStatusEffects) {
|
||||
public PlayerData(UUID playerUUID, UUID dataVersionUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth, int hunger, float saturation, float saturationExhaustion, int selectedSlot, String serializedStatusEffects) {
|
||||
this.playerUUID = playerUUID;
|
||||
this.dataVersionUUID = dataVersionUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
@@ -61,13 +62,14 @@ public class PlayerData implements Serializable {
|
||||
this.maxHealth = maxHealth;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
}
|
||||
|
||||
public static PlayerData EMPTY_PLAYER_DATA(UUID playerUUID) {
|
||||
public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
|
||||
return new PlayerData(playerUUID, "", "", 20,
|
||||
20, 20, 20, 0, "");
|
||||
20, 20, 10, 1, 0, "");
|
||||
}
|
||||
|
||||
public UUID getPlayerUUID() {
|
||||
@@ -102,6 +104,8 @@ public class PlayerData implements Serializable {
|
||||
return saturation;
|
||||
}
|
||||
|
||||
public float getSaturationExhaustion() { return saturationExhaustion; }
|
||||
|
||||
public int getSelectedSlot() {
|
||||
return selectedSlot;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,8 @@ public class RedisMessage {
|
||||
return messageData;
|
||||
}
|
||||
|
||||
public String[] getMessageDataElements() { return messageData.split(MESSAGE_DATA_SEPARATOR); }
|
||||
|
||||
public MessageType getMessageType() {
|
||||
return messageType;
|
||||
}
|
||||
@@ -100,7 +102,12 @@ public class RedisMessage {
|
||||
/**
|
||||
* Sent by the Proxy to reply to a {@code MessageType.PLAYER_DATA_REQUEST}, contains the latest {@link PlayerData} for the requester.
|
||||
*/
|
||||
PLAYER_DATA_REPLY
|
||||
PLAYER_DATA_SET,
|
||||
|
||||
/**
|
||||
* Sent by the proxy to ask the Bukkit server to send the full plugin information, contains information about the proxy brand and version
|
||||
*/
|
||||
SEND_PLUGIN_INFORMATION
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 182 KiB |
Reference in New Issue
Block a user