Add information command

feat/data-edit-commands
William 3 years ago
parent ba8e4ee175
commit 9198cd648f

@ -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,19 +37,33 @@ 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)) {
try {
// Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
switch (message.getMessageType()) {
case PLAYER_DATA_SET -> {
try {
// Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
// Set the player's data
PlayerSetter.setPlayerFrom(player, data);
// Set the player's data
PlayerSetter.setPlayerFrom(player, data);
// Update last loaded data UUID
CrossServerSyncBukkit.lastDataUpdateUUIDCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID());
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a reply from the proxy with PlayerData");
e.printStackTrace();
// Update last loaded data UUID
CrossServerSyncBukkit.lastDataUpdateUUIDCache.setVersionUUID(player.getUniqueId(), data.getDataVersionUUID());
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a reply from the proxy with PlayerData");
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

Loading…
Cancel
Save