9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-26 01:59:20 +00:00

1.0 release progress

This commit is contained in:
William
2021-10-24 21:46:48 +01:00
parent f842afac1e
commit f7f1dc50eb
35 changed files with 1780 additions and 245 deletions

View File

@@ -4,17 +4,76 @@ import me.william278.husksync.bukkit.config.ConfigLoader;
import me.william278.husksync.bukkit.data.BukkitDataCache;
import me.william278.husksync.bukkit.listener.BukkitRedisListener;
import me.william278.husksync.bukkit.listener.EventListener;
import me.william278.husksync.bukkit.migrator.MPDBDeserializer;
import me.william278.husksync.redis.RedisMessage;
import org.bukkit.Bukkit;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.scheduler.BukkitTask;
import java.io.IOException;
import java.util.UUID;
import java.util.logging.Level;
public final class HuskSyncBukkit extends JavaPlugin {
private static HuskSyncBukkit instance;
public static HuskSyncBukkit getInstance() {
return instance;
}
public static BukkitDataCache bukkitCache;
// Used for establishing a handshake with redis
public static UUID serverUUID;
// Has a handshake been established with the Bungee?
public static boolean handshakeCompleted = false;
// THe handshake task to execute
private static BukkitTask handshakeTask;
// Whether MySqlPlayerDataBridge is installed
public static boolean isMySqlPlayerDataBridgeInstalled;
// Establish the handshake with the proxy
public static void establishRedisHandshake() {
serverUUID = UUID.randomUUID();
getInstance().getLogger().log(Level.INFO, "Executing handshake with Proxy server...");
final int[] attempts = {0}; // How many attempts to establish communication have been made
handshakeTask = Bukkit.getScheduler().runTaskTimerAsynchronously(getInstance(), () -> {
if (handshakeCompleted) {
handshakeTask.cancel();
return;
}
try {
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
serverUUID.toString(),
Boolean.toString(isMySqlPlayerDataBridgeInstalled),
Bukkit.getName()).send();
attempts[0]++;
if (attempts[0] == 10) {
getInstance().getLogger().log(Level.WARNING, "Failed to complete handshake with the Proxy server; Please make sure your Proxy server is online and has HuskSync installed in its' /plugins/ folder. HuskSync will continue to try and establish a connection.");
}
} catch (IOException e) {
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake establishment", e);
}
}, 0, 60);
}
private void closeRedisHandshake() {
try {
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
serverUUID.toString(),
Bukkit.getName()).send();
} catch (IOException e) {
getInstance().getLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
}
}
@Override
public void onLoad() {
instance = this;
@@ -31,6 +90,14 @@ public final class HuskSyncBukkit extends JavaPlugin {
reloadConfig();
ConfigLoader.loadSettings(getConfig());
// Check if MySqlPlayerDataBridge is installed
Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
if (mySqlPlayerDataBridge != null) {
isMySqlPlayerDataBridgeInstalled = mySqlPlayerDataBridge.isEnabled();
MPDBDeserializer.setMySqlPlayerDataBridge();
getLogger().info("MySQLPlayerDataBridge detected! Disabled data synchronisation to prevent data loss. To perform a migration, run \"husksync migrate\" in your Proxy (Bungeecord, Waterfall, etc) server console.");
}
// Initialize last data update UUID cache
bukkitCache = new BukkitDataCache();
@@ -38,7 +105,14 @@ public final class HuskSyncBukkit extends JavaPlugin {
getServer().getPluginManager().registerEvents(new EventListener(), this);
// Initialize the redis listener
new BukkitRedisListener();
if (!new BukkitRedisListener().isActiveAndEnabled) {
getPluginLoader().disablePlugin(this);
getLogger().severe("Failed to initialize Redis; disabling HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
return;
}
// Ensure redis is connected; establish a handshake
establishRedisHandshake();
// Log to console
getLogger().info("Enabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
@@ -46,6 +120,9 @@ public final class HuskSyncBukkit extends JavaPlugin {
@Override
public void onDisable() {
// Send termination handshake to proxy
closeRedisHandshake();
// Plugin shutdown logic
getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
}

View File

@@ -1,17 +1,17 @@
package me.william278.husksync.bukkit;
import de.themoep.minedown.MineDown;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.MessageStrings;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import net.md_5.bungee.api.ChatMessageType;
import me.william278.husksync.bukkit.data.DataSerializer;
import me.william278.husksync.redis.RedisMessage;
import org.bukkit.*;
import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
@@ -19,12 +19,19 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.UUID;
import java.util.logging.Level;
public class PlayerSetter {
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
public static void requestPlayerData(UUID playerUUID) throws IOException {
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
playerUUID.toString()).send();
}
/**
* Set a player from their PlayerData, based on settings
*
@@ -34,15 +41,23 @@ public class PlayerSetter {
public static void setPlayerFrom(Player player, PlayerData data) {
// If the data is flagged as being default data, skip setting
if (data.isUseDefaultData()) {
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
return;
}
// Clear player
player.getInventory().clear();
player.getEnderChest().clear();
player.setExp(0);
player.setLevel(0);
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
// Set the player's data from the PlayerData
Bukkit.getScheduler().runTask(plugin, () -> {
try {
if (Settings.syncAdvancements) {
// Sync advancements first so that any rewards will be overridden
setPlayerAdvancements(player, DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements()));
setPlayerAdvancements(player, DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements()), data);
}
if (Settings.syncInventories) {
setPlayerInventory(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedInventory()));
@@ -52,6 +67,7 @@ public class PlayerSetter {
setPlayerEnderChest(player, DataSerializer.itemStackArrayFromBase64(data.getSerializedEnderChest()));
}
if (Settings.syncHealth) {
player.setHealthScale(data.getHealthScale() > 0 ? data.getHealthScale() : 0D);
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(data.getMaxHealth());
player.setHealth(data.getHealth());
}
@@ -61,9 +77,8 @@ public class PlayerSetter {
player.setExhaustion(data.getSaturationExhaustion());
}
if (Settings.syncExperience) {
player.setTotalExperience(data.getTotalExperience());
player.setLevel(data.getExpLevel());
player.setExp(data.getExpProgress());
// This is also handled when syncing advancements to ensure its correct
setPlayerExperience(player, data);
}
if (Settings.syncPotionEffects) {
setPlayerPotionEffects(player, DataSerializer.potionEffectArrayFromBase64(data.getSerializedEffectData()));
@@ -78,9 +93,6 @@ public class PlayerSetter {
player.setFlying(player.getAllowFlight() && data.isFlying());
setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
}
// Send action bar synchronisation message
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageStrings.SYNCHRONISATION_COMPLETE).toComponent());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
}
@@ -94,14 +106,7 @@ public class PlayerSetter {
* @param items The array of {@link ItemStack}s to set
*/
private static void setPlayerEnderChest(Player player, ItemStack[] items) {
player.getEnderChest().clear();
int index = 0;
for (ItemStack item : items) {
if (item != null) {
player.getEnderChest().setItem(index, item);
}
index++;
}
setInventory(player.getEnderChest(), items);
}
/**
@@ -111,11 +116,21 @@ public class PlayerSetter {
* @param items The array of {@link ItemStack}s to set
*/
private static void setPlayerInventory(Player player, ItemStack[] items) {
player.getInventory().clear();
setInventory(player.getInventory(), items);
}
/**
* Sets an inventory's contents from an array of {@link ItemStack}s
*
* @param inventory The inventory to set
* @param items The {@link ItemStack}s to fill it with
*/
public static void setInventory(Inventory inventory, ItemStack[] items) {
inventory.clear();
int index = 0;
for (ItemStack item : items) {
if (item != null) {
player.getInventory().setItem(index, item);
inventory.setItem(index, item);
}
index++;
}
@@ -142,7 +157,7 @@ public class PlayerSetter {
* @param player The player to set the advancements of
* @param advancementData The ArrayList of {@link DataSerializer.AdvancementRecord}s to set
*/
private static void setPlayerAdvancements(Player player, ArrayList<DataSerializer.AdvancementRecord> advancementData) {
private static void setPlayerAdvancements(Player player, ArrayList<DataSerializer.AdvancementRecord> advancementData, PlayerData data) {
// Temporarily disable advancement announcing if needed
boolean announceAdvancementUpdate = false;
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
@@ -153,36 +168,39 @@ public class PlayerSetter {
// Run async because advancement loading is very slow
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Apply the advancements to the player
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
while (serverAdvancements.hasNext()) {
while (serverAdvancements.hasNext()) { // Iterate through all advancements
boolean correctExperienceCheck = false; // Determines whether the experience might have changed warranting an update
Advancement advancement = serverAdvancements.next();
AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
boolean hasAdvancement = false;
for (DataSerializer.AdvancementRecord record : advancementData) {
// If the advancement is one on the data
if (record.advancementKey().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
hasAdvancement = true;
// Save the experience before granting the advancement
final int expLevel = player.getLevel();
final float expProgress = player.getExp();
// Grant advancement criteria if the player does not have it
// Award all criteria that the player does not have that they do on the cache
ArrayList<String> currentlyAwardedCriteria = new ArrayList<>(playerProgress.getAwardedCriteria());
for (String awardCriteria : record.awardedAdvancementCriteria()) {
if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
correctExperienceCheck = true;
}
currentlyAwardedCriteria.remove(awardCriteria);
}
// Set experience back to before granting advancement; nullify exp gained from it
player.setLevel(expLevel);
player.setExp(expProgress);
// Revoke all criteria that the player does have but should not
for (String awardCriteria : currentlyAwardedCriteria) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
}
break;
}
}
if (!hasAdvancement) {
for (String awardCriteria : playerProgress.getAwardedCriteria()) {
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).revokeCriteria(awardCriteria));
// Update the player's experience in case the advancement changed that
if (correctExperienceCheck) {
if (Settings.syncExperience) {
setPlayerExperience(player, data);
}
}
}
@@ -230,6 +248,18 @@ public class PlayerSetter {
}
}
/**
* Set a player's exp level, exp points & score
*
* @param player The {@link Player} to set
* @param data The {@link PlayerData} to set them
*/
private static void setPlayerExperience(Player player, PlayerData data) {
player.setTotalExperience(data.getTotalExperience());
player.setLevel(data.getExpLevel());
player.setExp(data.getExpProgress());
}
/**
* Set a player's location from {@link DataSerializer.PlayerLocation} data
*

View File

@@ -1,5 +1,6 @@
package me.william278.husksync.bukkit.data;
import java.util.HashMap;
import java.util.HashSet;
import java.util.UUID;
@@ -10,10 +11,6 @@ public class BukkitDataCache {
*/
private static HashSet<UUID> requestOnJoin;
public BukkitDataCache() {
requestOnJoin = new HashSet<>();
}
public boolean isPlayerRequestingOnJoin(UUID uuid) {
return requestOnJoin.contains(uuid);
}
@@ -25,4 +22,53 @@ public class BukkitDataCache {
public void removeRequestOnJoin(UUID uuid) {
requestOnJoin.remove(uuid);
}
}
/**
* Map of Player UUIDs whose data has not been set yet
*/
private static HashSet<UUID> awaitingDataFetch;
public boolean isAwaitingDataFetch(UUID uuid) {
return awaitingDataFetch.contains(uuid);
}
public void setAwaitingDataFetch(UUID uuid) {
awaitingDataFetch.add(uuid);
}
public void removeAwaitingDataFetch(UUID uuid) {
awaitingDataFetch.remove(uuid);
}
public HashSet<UUID> getAwaitingDataFetch() {
return awaitingDataFetch;
}
/**
* Map of data being viewed by players
*/
private static HashMap<UUID, DataViewer.DataView> viewingPlayerData;
public void setViewing(UUID uuid, DataViewer.DataView dataView) {
viewingPlayerData.put(uuid, dataView);
}
public void removeViewing(UUID uuid) {
viewingPlayerData.remove(uuid);
}
public boolean isViewing(UUID uuid) {
return viewingPlayerData.containsKey(uuid);
}
public DataViewer.DataView getViewing(UUID uuid) {
return viewingPlayerData.get(uuid);
}
// Cache object
public BukkitDataCache() {
requestOnJoin = new HashSet<>();
viewingPlayerData = new HashMap<>();
awaitingDataFetch = new HashSet<>();
}
}

View File

@@ -1,4 +1,4 @@
package me.william278.husksync.bukkit;
package me.william278.husksync.bukkit.data;
import me.william278.husksync.redis.RedisMessage;
import org.bukkit.*;
@@ -6,6 +6,7 @@ import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.potion.PotionEffect;
import org.bukkit.util.io.BukkitObjectInputStream;
@@ -33,19 +34,19 @@ public final class DataSerializer {
/**
* Converts the player inventory to a Base64 encoded string.
*
* @param player whose inventory will be turned into an array of strings.
* @param inventory the inventory to convert to Base64.
* @return string with serialized Inventory
* @throws IllegalStateException in the event the item stacks cannot be saved
*/
public static String getSerializedInventoryContents(Player player) throws IllegalStateException {
public static String getSerializedInventoryContents(Inventory inventory) throws IllegalStateException {
// This contains contents, armor and offhand (contents are indexes 0 - 35, armor 36 - 39, offhand - 40)
return itemStackArrayToBase64(player.getInventory().getContents());
return itemStackArrayToBase64(inventory.getContents());
}
/**
* Converts the player inventory to a Base64 encoded string.
*
* @param player whose Ender Chest will be turned into an array of strings.
* @param player whose Ender Chest will be turned into Base64.
* @return string with serialized Ender Chest
* @throws IllegalStateException in the event the item stacks cannot be saved
*/

View File

@@ -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());
};
}
}
}

View File

@@ -1,12 +1,16 @@
package me.william278.husksync.bukkit.listener;
import de.themoep.minedown.MineDown;
import me.william278.husksync.PlayerData;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.bukkit.PlayerSetter;
import me.william278.husksync.redis.RedisListener;
import me.william278.husksync.MessageStrings;
import me.william278.husksync.MessageManager;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.bukkit.config.ConfigLoader;
import me.william278.husksync.bukkit.data.DataViewer;
import me.william278.husksync.bukkit.PlayerSetter;
import me.william278.husksync.bukkit.migrator.MPDBDeserializer;
import me.william278.husksync.migrator.MPDBPlayerData;
import me.william278.husksync.redis.RedisListener;
import me.william278.husksync.redis.RedisMessage;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@@ -25,25 +29,74 @@ public class BukkitRedisListener extends RedisListener {
}
/**
* Handle an incoming {@link me.william278.husksync.redis.RedisMessage}
* Handle an incoming {@link RedisMessage}
*
* @param message The {@link me.william278.husksync.redis.RedisMessage} to handle
* @param message The {@link RedisMessage} to handle
*/
@Override
public void handleMessage(me.william278.husksync.redis.RedisMessage message) {
public void handleMessage(RedisMessage message) {
// Ignore messages for proxy servers
if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
return;
}
// Handle the message for the player
if (message.getMessageTarget().targetPlayerUUID() == null) {
if (message.getMessageType() == me.william278.husksync.redis.RedisMessage.MessageType.REQUEST_DATA_ON_JOIN) {
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
switch (me.william278.husksync.redis.RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
case ADD_REQUESTER -> HuskSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
case REMOVE_REQUESTER -> HuskSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
switch (message.getMessageType()) {
case REQUEST_DATA_ON_JOIN -> {
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
switch (me.william278.husksync.redis.RedisMessage.RequestOnJoinUpdateType.valueOf(message.getMessageDataElements()[0])) {
case ADD_REQUESTER -> HuskSyncBukkit.bukkitCache.setRequestOnJoin(playerUUID);
case REMOVE_REQUESTER -> HuskSyncBukkit.bukkitCache.removeRequestOnJoin(playerUUID);
}
}
case CONNECTION_HANDSHAKE -> {
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
String proxyBrand = message.getMessageDataElements()[1];
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
HuskSyncBukkit.handshakeCompleted = true;
log(Level.INFO, "Completed handshake with " + proxyBrand + " proxy (" + serverUUID + ")");
// If there are any players awaiting a data update, request it
for (UUID uuid : HuskSyncBukkit.bukkitCache.getAwaitingDataFetch()) {
try {
PlayerSetter.requestPlayerData(uuid);
} catch (IOException e) {
log(Level.SEVERE, "Failed to serialize handshake message data");
}
}
}
}
case TERMINATE_HANDSHAKE -> {
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
String proxyBrand = message.getMessageDataElements()[1];
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
HuskSyncBukkit.handshakeCompleted = false;
log(Level.WARNING, proxyBrand + " proxy has terminated communications; attempting to re-establish (" + serverUUID + ")");
// Attempt to re-establish communications via another handshake
Bukkit.getScheduler().runTaskLaterAsynchronously(plugin, HuskSyncBukkit::establishRedisHandshake, 20);
}
}
case DECODE_MPDB_DATA -> {
UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
String encodedData = message.getMessageDataElements()[1];
if (serverUUID.equals(HuskSyncBukkit.serverUUID)) {
try {
MPDBPlayerData data = (MPDBPlayerData) RedisMessage.deserialize(encodedData);
new RedisMessage(RedisMessage.MessageType.DECODED_MPDB_DATA_SET,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
data.playerName)
.send();
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to serialize encoded MPDB data");
}
}
}
case RELOAD_CONFIG -> {
plugin.reloadConfig();
ConfigLoader.loadSettings(plugin.getConfig());
}
}
} else {
@@ -51,6 +104,7 @@ public class BukkitRedisListener extends RedisListener {
if (player.getUniqueId().equals(message.getMessageTarget().targetPlayerUUID())) {
switch (message.getMessageType()) {
case PLAYER_DATA_SET -> {
if (HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return;
try {
// Deserialize the received PlayerData
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageData());
@@ -58,7 +112,7 @@ public class BukkitRedisListener extends RedisListener {
// Set the player's data
PlayerSetter.setPlayerFrom(player, data);
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a reply from the proxy with PlayerData");
log(Level.SEVERE, "Failed to deserialize PlayerData when handling data from the proxy");
e.printStackTrace();
}
}
@@ -66,7 +120,7 @@ public class BukkitRedisListener extends RedisListener {
String proxyBrand = message.getMessageDataElements()[0];
String proxyVersion = message.getMessageDataElements()[1];
assert plugin.getDescription().getDescription() != null;
player.spigot().sendMessage(new MineDown(MessageStrings.PLUGIN_INFORMATION.toString()
player.spigot().sendMessage(new MineDown(MessageManager.PLUGIN_INFORMATION.toString()
.replaceAll("%plugin_description%", plugin.getDescription().getDescription())
.replaceAll("%proxy_brand%", proxyBrand)
.replaceAll("%proxy_version%", proxyVersion)
@@ -74,6 +128,42 @@ public class BukkitRedisListener extends RedisListener {
.replaceAll("%bukkit_version%", plugin.getDescription().getVersion()))
.toComponent());
}
case OPEN_INVENTORY -> {
// Get the name of the inventory owner
String inventoryOwnerName = message.getMessageDataElements()[0];
// Synchronously do inventory setting, etc
Bukkit.getScheduler().runTask(plugin, () -> {
try {
// Get that player's data
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
// Show the data to the player
DataViewer.showData(player, new DataViewer.DataView(data, inventoryOwnerName, DataViewer.DataView.InventoryType.INVENTORY));
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling inventory-see data from the proxy");
e.printStackTrace();
}
});
}
case OPEN_ENDER_CHEST -> {
// Get the name of the inventory owner
String enderChestOwnerName = message.getMessageDataElements()[0];
// Synchronously do inventory setting, etc
Bukkit.getScheduler().runTask(plugin, () -> {
try {
// Get that player's data
PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
// Show the data to the player
DataViewer.showData(player, new DataViewer.DataView(data, enderChestOwnerName, DataViewer.DataView.InventoryType.ENDER_CHEST));
} catch (IOException | ClassNotFoundException e) {
log(Level.SEVERE, "Failed to deserialize PlayerData when handling ender chest-see data from the proxy");
e.printStackTrace();
}
});
}
}
return;
}

View File

@@ -3,14 +3,22 @@ package me.william278.husksync.bukkit.listener;
import me.william278.husksync.HuskSyncBukkit;
import me.william278.husksync.PlayerData;
import me.william278.husksync.Settings;
import me.william278.husksync.bukkit.DataSerializer;
import me.william278.husksync.bukkit.data.DataSerializer;
import me.william278.husksync.bukkit.data.DataViewer;
import me.william278.husksync.bukkit.PlayerSetter;
import me.william278.husksync.redis.RedisMessage;
import org.bukkit.Bukkit;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryOpenEvent;
import org.bukkit.event.player.*;
import java.io.IOException;
import java.util.Objects;
@@ -28,11 +36,12 @@ public class EventListener implements Listener {
* @throws IOException If the serialization fails
*/
private static String getNewSerializedPlayerData(Player player) throws IOException {
return me.william278.husksync.redis.RedisMessage.serialize(new PlayerData(player.getUniqueId(),
DataSerializer.getSerializedInventoryContents(player),
return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
DataSerializer.getSerializedInventoryContents(player.getInventory()),
DataSerializer.getSerializedEnderChestContents(player),
player.getHealth(),
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue(),
player.getHealthScale(),
player.getFoodLevel(),
player.getSaturation(),
player.getExhaustion(),
@@ -53,11 +62,19 @@ public class EventListener implements Listener {
// When a player leaves a Bukkit server
final Player player = event.getPlayer();
// If the player was awaiting data fetch, remove them and prevent data from being overwritten
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
HuskSyncBukkit.bukkitCache.removeAwaitingDataFetch(player.getUniqueId());
return;
}
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return; // If the plugin has not been initialized correctly
// Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
try {
final String serializedPlayerData = getNewSerializedPlayerData(player);
new me.william278.husksync.redis.RedisMessage(me.william278.husksync.redis.RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new me.william278.husksync.redis.RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
serializedPlayerData).send();
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
@@ -70,22 +87,101 @@ public class EventListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
if (!plugin.isEnabled()) return; // If the plugin has not been initialized correctly
// When a player joins a Bukkit server
final Player player = event.getPlayer();
// Clear player inventory and ender chest
player.getInventory().clear();
player.getEnderChest().clear();
// Mark the player as awaiting data fetch
HuskSyncBukkit.bukkitCache.setAwaitingDataFetch(player.getUniqueId());
if (!HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled) return; // If the data handshake has not been completed yet (or MySqlPlayerDataBridge is installed)
// Send a redis message requesting the player data (if they need to)
if (HuskSyncBukkit.bukkitCache.isPlayerRequestingOnJoin(player.getUniqueId())) {
try {
// Send a redis message requesting the player data
new me.william278.husksync.redis.RedisMessage(me.william278.husksync.redis.RedisMessage.MessageType.PLAYER_DATA_REQUEST,
new RedisMessage.MessageTarget(Settings.ServerType.BUNGEECORD, null),
player.getUniqueId().toString()).send();
PlayerSetter.requestPlayerData(player.getUniqueId());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
}
} else {
// If the player's data wasn't set after 10 ticks, ensure it will be
Bukkit.getScheduler().scheduleSyncDelayedTask(plugin, () -> {
if (player.isOnline()) {
try {
if (HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
PlayerSetter.requestPlayerData(player.getUniqueId());
}
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData fetch request", e);
}
}
}, 5);
}
}
@EventHandler
public void onInventoryClose(InventoryCloseEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) return; // If the plugin has not been initialized correctly
// When a player closes an Inventory
final Player player = (Player) event.getPlayer();
// Handle a player who has finished viewing a player's item data
if (HuskSyncBukkit.bukkitCache.isViewing(player.getUniqueId())) {
try {
DataViewer.stopShowing(player, event.getInventory());
} catch (IOException e) {
plugin.getLogger().log(Level.SEVERE, "Failed to serialize updated item data", e);
}
}
}
/*
* Events to cancel if the player has not been set yet
*/
@EventHandler(priority = EventPriority.MONITOR)
public void onDropItem(PlayerDropItemEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPickupItem(EntityPickupItemEvent event) {
if (event.getEntity() instanceof Player player) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(player.getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerInteract(PlayerInteractEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onBlockPlace(BlockPlaceEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onBlockBreak(BlockBreakEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onInventoryOpen(InventoryOpenEvent event) {
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted || HuskSyncBukkit.bukkitCache.isAwaitingDataFetch(event.getPlayer().getUniqueId())) {
event.setCancelled(true); // If the plugin / player has not been set
}
}
}

View File

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