diff --git a/README.md b/README.md index cc26c3fc..02be121f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bukkit/build.gradle b/bukkit/build.gradle index 012a8d47..6bb021c1 100644 --- a/bukkit/build.gradle +++ b/bukkit/build.gradle @@ -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'){} \ No newline at end of file diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java index 6b1c863a..7d874d7b 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/PlayerSetter.java @@ -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) { diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java index 0e88f50c..2742016a 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/BukkitRedisListener.java @@ -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; diff --git a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java index 6193e1b5..5e1fd87b 100644 --- a/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java +++ b/bukkit/src/main/java/me/william278/crossserversync/bukkit/listener/EventListener.java @@ -33,6 +33,7 @@ public class EventListener implements Listener { player.getMaxHealth(), player.getFoodLevel(), player.getSaturation(), + player.getExhaustion(), player.getInventory().getHeldItemSlot(), DataSerializer.getSerializedEffectData(player))); } diff --git a/bungeecord/build.gradle b/bungeecord/build.gradle index f02974d0..60cd7c7a 100644 --- a/bungeecord/build.gradle +++ b/bungeecord/build.gradle @@ -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'){} \ No newline at end of file diff --git a/bungeecord/src/main/java/me/william278/crossserversync/CrossServerSyncBungeeCord.java b/bungeecord/src/main/java/me/william278/crossserversync/CrossServerSyncBungeeCord.java index 1a4db168..1ba50b74 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/CrossServerSyncBungeeCord.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/CrossServerSyncBungeeCord.java @@ -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(); diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java new file mode 100644 index 00000000..8a588707 --- /dev/null +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/command/CrossServerSyncCommand.java @@ -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 ")).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 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(); + } + +} diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java index ae1f7809..29b56db0 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/DataManager.java @@ -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(); } diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java index 46a281d7..49ee66f6 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/MySQL.java @@ -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," + diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java index 53c89a4c..953b6efd 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/data/sql/SQLite.java @@ -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," + diff --git a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java index 9e56ddda..cd098190 100644 --- a/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java +++ b/bungeecord/src/main/java/me/william278/crossserversync/bungeecord/listener/BungeeRedisListener.java @@ -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) { diff --git a/common/src/main/java/me/william278/crossserversync/MessageStrings.java b/common/src/main/java/me/william278/crossserversync/MessageStrings.java new file mode 100644 index 00000000..ae9709b7 --- /dev/null +++ b/common/src/main/java/me/william278/crossserversync/MessageStrings.java @@ -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)"; + +} \ No newline at end of file diff --git a/common/src/main/java/me/william278/crossserversync/PlayerData.java b/common/src/main/java/me/william278/crossserversync/PlayerData.java index 87333de8..169accc4 100644 --- a/common/src/main/java/me/william278/crossserversync/PlayerData.java +++ b/common/src/main/java/me/william278/crossserversync/PlayerData.java @@ -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; } diff --git a/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java b/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java index 9fed3cf8..dca892af 100644 --- a/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java +++ b/common/src/main/java/me/william278/crossserversync/redis/RedisMessage.java @@ -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 } /** diff --git a/images/flow-chart.png b/images/flow-chart.png index 3e9527d6..9915a280 100644 Binary files a/images/flow-chart.png and b/images/flow-chart.png differ