forked from public-mirrors/HuskSync
Start 2.0 rewrite
Use redis key caching, remove need for proxy plugin Make platform independent to allow porting to other platformsfeat/data-edit-commands
parent
633847a254
commit
9471e0cbff
@ -1,81 +0,0 @@
|
||||
package net.william278.husksync.bukkit.api;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* HuskSync's API. To access methods, use the {@link #getInstance()} entrypoint.
|
||||
*
|
||||
* @author William
|
||||
*/
|
||||
public class HuskSyncAPI {
|
||||
|
||||
private HuskSyncAPI() {
|
||||
}
|
||||
|
||||
private static HuskSyncAPI instance;
|
||||
|
||||
/**
|
||||
* The API entry point. Returns an instance of the {@link HuskSyncAPI}
|
||||
*
|
||||
* @return instance of the {@link HuskSyncAPI}
|
||||
*/
|
||||
public static HuskSyncAPI getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new HuskSyncAPI();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link CompletableFuture} that will fetch the {@link PlayerData} for a user given their {@link UUID},
|
||||
* which contains serialized synchronised data.
|
||||
* <p>
|
||||
* This can then be deserialized into ItemStacks and other usable values using the {@code DataSerializer} class.
|
||||
* <p>
|
||||
* If no data could be returned, such as if an invalid UUID is specified, the CompletableFuture will be cancelled.
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the player to get data for
|
||||
* @return a {@link CompletableFuture} with the user's {@link PlayerData} accessible on completion
|
||||
* @throws IOException If an exception occurs with serializing during processing of the request
|
||||
* @apiNote This only returns the latest saved and cached data of the user. This is <b>not</b> necessarily the current state of their inventory if they are online.
|
||||
*/
|
||||
public CompletableFuture<PlayerData> getPlayerData(UUID playerUUID) throws IOException {
|
||||
// Create the request to be completed
|
||||
final UUID requestUUID = UUID.randomUUID();
|
||||
BukkitRedisListener.apiRequests.put(requestUUID, new CompletableFuture<>());
|
||||
|
||||
// Remove the request from the map on completion
|
||||
BukkitRedisListener.apiRequests.get(requestUUID).whenComplete((playerData, throwable) -> BukkitRedisListener.apiRequests.remove(requestUUID));
|
||||
|
||||
// Request the data via the proxy
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_REQUEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
playerUUID.toString(), requestUUID.toString()).send();
|
||||
|
||||
return BukkitRedisListener.apiRequests.get(requestUUID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a player's {@link PlayerData} to the proxy cache and database.
|
||||
* <p>
|
||||
* If the player is online on the Proxy network, they will be updated and overwritten with this data.
|
||||
*
|
||||
* @param playerData The {@link PlayerData} (which contains the {@link UUID}) of the player data to update to the central cache and database
|
||||
* @throws IOException If an exception occurs with serializing during processing of the update
|
||||
*/
|
||||
public void updatePlayerData(PlayerData playerData) throws IOException {
|
||||
// Serialize and send the updated player data
|
||||
final String serializedPlayerData = RedisMessage.serialize(playerData);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serializedPlayerData, Boolean.toString(true)).send();
|
||||
}
|
||||
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package me.william278.husksync.bukkit.data;
|
||||
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.EntityType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Holds legacy data store methods for data storage
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
public class DataSerializer {
|
||||
|
||||
/**
|
||||
* A record used to store data for advancement synchronisation
|
||||
*
|
||||
* @deprecated Old format - Use {@link AdvancementRecordDate} instead
|
||||
*/
|
||||
@Deprecated
|
||||
@SuppressWarnings("DeprecatedIsStillUsed")
|
||||
// Suppress deprecation warnings here (still used for backwards compatibility)
|
||||
public record AdvancementRecord(String advancementKey,
|
||||
ArrayList<String> awardedAdvancementCriteria) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for a player's statistics
|
||||
*/
|
||||
public record StatisticData(HashMap<Statistic, Integer> untypedStatisticValues,
|
||||
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues,
|
||||
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues,
|
||||
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for native advancement synchronisation, tracking advancement date progress
|
||||
*/
|
||||
public record AdvancementRecordDate(String key, Map<String, Date> criteriaMap) implements Serializable {
|
||||
public AdvancementRecordDate(String key, List<String> criteriaList) {
|
||||
this(key, new HashMap<>() {{
|
||||
criteriaList.forEach(s -> put(s, Date.from(Instant.EPOCH)));
|
||||
}});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A record used to store data for a player's location
|
||||
*/
|
||||
public record PlayerLocation(double x, double y, double z, float yaw, float pitch,
|
||||
String worldName, World.Environment environment) implements Serializable {
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.util.BukkitUpdateChecker;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.bukkit.config.ConfigLoader;
|
||||
import net.william278.husksync.bukkit.data.BukkitDataCache;
|
||||
import net.william278.husksync.bukkit.listener.BukkitRedisListener;
|
||||
import net.william278.husksync.bukkit.listener.BukkitEventListener;
|
||||
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
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 {
|
||||
|
||||
// Bukkit bStats ID (Different to BungeeCord)
|
||||
private static final int METRICS_ID = 13140;
|
||||
|
||||
private static HuskSyncBukkit instance;
|
||||
public static HuskSyncBukkit getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static BukkitDataCache bukkitCache;
|
||||
|
||||
public static BukkitRedisListener redisListener;
|
||||
|
||||
// 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.PROXY, null, Settings.cluster),
|
||||
serverUUID.toString(),
|
||||
Boolean.toString(isMySqlPlayerDataBridgeInstalled),
|
||||
Bukkit.getName(),
|
||||
getInstance().getDescription().getVersion())
|
||||
.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() {
|
||||
if (!handshakeCompleted) return;
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
|
||||
// Load the config file
|
||||
getConfig().options().copyDefaults(true);
|
||||
saveDefaultConfig();
|
||||
saveConfig();
|
||||
reloadConfig();
|
||||
ConfigLoader.loadSettings(getConfig());
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new BukkitUpdateChecker().logToConsole();
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// Initialize event listener
|
||||
getServer().getPluginManager().registerEvents(new BukkitEventListener(), this);
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new BukkitRedisListener();
|
||||
|
||||
// Ensure redis is connected; establish a handshake
|
||||
establishRedisHandshake();
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getLogger().info("Enabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Update player data for disconnecting players
|
||||
if (HuskSyncBukkit.handshakeCompleted && !HuskSyncBukkit.isMySqlPlayerDataBridgeInstalled && Bukkit.getOnlinePlayers().size() > 0) {
|
||||
getLogger().info("Saving data for remaining online players...");
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
PlayerSetter.updatePlayerData(player, false);
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
}
|
||||
getLogger().info("Data save complete!");
|
||||
}
|
||||
|
||||
|
||||
// Send termination handshake to proxy
|
||||
closeRedisHandshake();
|
||||
|
||||
// Plugin shutdown logic
|
||||
getLogger().info("Disabled HuskSync (" + getServer().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
package net.william278.husksync.bukkit.config;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
public static void loadSettings(FileConfiguration config) throws IllegalArgumentException {
|
||||
Settings.serverType = Settings.ServerType.BUKKIT;
|
||||
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
|
||||
Settings.cluster = config.getString("cluster_id", "main");
|
||||
Settings.redisHost = config.getString("redis_settings.host", "localhost");
|
||||
Settings.redisPort = config.getInt("redis_settings.port", 6379);
|
||||
Settings.redisPassword = config.getString("redis_settings.password", "");
|
||||
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
|
||||
|
||||
Settings.syncInventories = config.getBoolean("synchronisation_settings.inventories", true);
|
||||
Settings.syncEnderChests = config.getBoolean("synchronisation_settings.ender_chests", true);
|
||||
Settings.syncHealth = config.getBoolean("synchronisation_settings.health", true);
|
||||
Settings.syncHunger = config.getBoolean("synchronisation_settings.hunger", true);
|
||||
Settings.syncExperience = config.getBoolean("synchronisation_settings.experience", true);
|
||||
Settings.syncPotionEffects = config.getBoolean("synchronisation_settings.potion_effects", true);
|
||||
Settings.syncStatistics = config.getBoolean("synchronisation_settings.statistics", true);
|
||||
Settings.syncGameMode = config.getBoolean("synchronisation_settings.game_mode", true);
|
||||
Settings.syncAdvancements = config.getBoolean("synchronisation_settings.advancements", true);
|
||||
Settings.syncLocation = config.getBoolean("synchronisation_settings.location", false);
|
||||
Settings.syncFlight = config.getBoolean("synchronisation_settings.flight", false);
|
||||
|
||||
Settings.useNativeImplementation = config.getBoolean("native_advancement_synchronization", false);
|
||||
Settings.saveOnWorldSave = config.getBoolean("save_on_world_save", true);
|
||||
Settings.synchronizationTimeoutRetryDelay = config.getLong("synchronization_timeout_retry_delay", 15L);
|
||||
}
|
||||
|
||||
}
|
@ -1,74 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
|
||||
public class BukkitDataCache {
|
||||
|
||||
/**
|
||||
* Map of Player UUIDs to request on join
|
||||
*/
|
||||
private static HashSet<UUID> requestOnJoin;
|
||||
|
||||
public boolean isPlayerRequestingOnJoin(UUID uuid) {
|
||||
return requestOnJoin.contains(uuid);
|
||||
}
|
||||
|
||||
public void setRequestOnJoin(UUID uuid) {
|
||||
requestOnJoin.add(uuid);
|
||||
}
|
||||
|
||||
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<>();
|
||||
}
|
||||
}
|
@ -1,327 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Class that contains static methods for serializing and deserializing data from {@link net.william278.husksync.PlayerData}
|
||||
*/
|
||||
public class DataSerializer {
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventoryContents The contents of the inventory
|
||||
* @return The serialized inventory contents
|
||||
*/
|
||||
public static String serializeInventory(ItemStack[] inventoryContents) {
|
||||
// Return an empty string if there is no inventory item data to serialize
|
||||
if (inventoryContents.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the inventory array to serialize
|
||||
bukkitOutputStream.writeInt(inventoryContents.length);
|
||||
|
||||
// Write each serialize each ItemStack to the output stream
|
||||
for (ItemStack inventoryItem : inventoryContents) {
|
||||
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize item stack data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
|
||||
*
|
||||
* @param inventoryData The serialized {@link ItemStack[]} array
|
||||
* @return The inventory contents as an array of {@link ItemStack}s
|
||||
* @throws IOException If the deserialization fails reading data from the InputStream
|
||||
* @throws ClassNotFoundException If the deserialization class cannot be found
|
||||
*/
|
||||
public static ItemStack[] deserializeInventory(String inventoryData) throws IOException, ClassNotFoundException {
|
||||
// Return empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (inventoryData.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized inventory contents
|
||||
return inventoryContents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param item The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializeItemStack(ItemStack item) {
|
||||
return item != null ? item.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedItemStack The serialized item stack; a String-Object map
|
||||
* @return The deserialized {@link ItemStack}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static ItemStack deserializeItemStack(Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link PotionEffect}s
|
||||
*
|
||||
* @param potionEffects The potion effect array
|
||||
* @return The serialized potion effects
|
||||
*/
|
||||
public static String serializePotionEffects(PotionEffect[] potionEffects) {
|
||||
// Return an empty string if there are no effects to serialize
|
||||
if (potionEffects.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the potion effect array to serialize
|
||||
bukkitOutputStream.writeInt(potionEffects.length);
|
||||
|
||||
// Write each serialize each PotionEffect to the output stream
|
||||
for (PotionEffect potionEffect : potionEffects) {
|
||||
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize potion effect data");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized potion effect data
|
||||
*
|
||||
* @param potionEffectData The serialized {@link PotionEffect[]} array
|
||||
* @return The {@link PotionEffect}s
|
||||
* @throws IOException If the deserialization fails reading data from the InputStream
|
||||
* @throws ClassNotFoundException If the deserialization class cannot be found
|
||||
*/
|
||||
public static PotionEffect[] deserializePotionEffects(String potionEffectData) throws IOException, ClassNotFoundException {
|
||||
// Return empty array if there is no potion effect data (don't apply any effects to the player)
|
||||
if (potionEffectData.isEmpty()) {
|
||||
return new PotionEffect[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the potion effects in the array from deserialized PotionEffect data
|
||||
int potionIndex = 0;
|
||||
for (PotionEffect ignored : potionEffects) {
|
||||
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
|
||||
potionIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized potion effect array
|
||||
return potionEffects;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param potionEffect The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializePotionEffect(PotionEffect potionEffect) {
|
||||
return potionEffect != null ? potionEffect.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedPotionEffect The serialized potion effect; a String-Object map
|
||||
* @return The deserialized {@link PotionEffect}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) {
|
||||
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
|
||||
}
|
||||
|
||||
public static me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation deserializePlayerLocationData(String serializedLocationData) throws IOException {
|
||||
if (serializedLocationData.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return (me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation) RedisMessage.deserialize(serializedLocationData);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSerializedLocation(Player player) throws IOException {
|
||||
final Location playerLocation = player.getLocation();
|
||||
return RedisMessage.serialize(new me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation(playerLocation.getX(), playerLocation.getY(), playerLocation.getZ(),
|
||||
playerLocation.getYaw(), playerLocation.getPitch(), player.getWorld().getName(), player.getWorld().getEnvironment()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a player's advancement data as serialized with {@link #getSerializedAdvancements(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} data.
|
||||
*
|
||||
* @param serializedAdvancementData The serialized advancement data {@link String}
|
||||
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate} for the player
|
||||
* @throws IOException If the deserialization fails
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the unchecked cast here
|
||||
public static List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> deserializeAdvancementData(String serializedAdvancementData) throws IOException {
|
||||
if (serializedAdvancementData.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
try {
|
||||
List<?> deserialize = (List<?>) RedisMessage.deserialize(serializedAdvancementData);
|
||||
|
||||
// Migrate old AdvancementRecord into date format
|
||||
if (!deserialize.isEmpty() && deserialize.get(0) instanceof me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord) {
|
||||
deserialize = ((List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecord>) deserialize).stream()
|
||||
.map(o -> new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(
|
||||
o.advancementKey(),
|
||||
o.awardedAdvancementCriteria()
|
||||
)).toList();
|
||||
}
|
||||
|
||||
return (List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate>) deserialize;
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of a player's advancements that can be deserialized with {@link #deserializeStatisticData(String)}
|
||||
*
|
||||
* @param player {@link Player} to serialize advancement data of
|
||||
* @return The serialized advancement data as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
public static String getSerializedAdvancements(Player player) throws IOException {
|
||||
Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
ArrayList<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData = new ArrayList<>();
|
||||
|
||||
while (serverAdvancements.hasNext()) {
|
||||
final AdvancementProgress progress = player.getAdvancementProgress(serverAdvancements.next());
|
||||
final NamespacedKey advancementKey = progress.getAdvancement().getKey();
|
||||
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
progress.getAwardedCriteria().forEach(s -> awardedCriteria.put(s, progress.getDateAwarded(s)));
|
||||
|
||||
advancementData.add(new me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate(advancementKey.getNamespace() + ":" + advancementKey.getKey(), awardedCriteria));
|
||||
}
|
||||
|
||||
return RedisMessage.serialize(advancementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes a player's statistic data as serialized with {@link #getSerializedStatisticData(Player)} into {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData}.
|
||||
*
|
||||
* @param serializedStatisticData The serialized statistic data {@link String}
|
||||
* @return The deserialized {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} for the player
|
||||
* @throws IOException If the deserialization fails
|
||||
*/
|
||||
public static me.william278.husksync.bukkit.data.DataSerializer.StatisticData deserializeStatisticData(String serializedStatisticData) throws IOException {
|
||||
if (serializedStatisticData.isEmpty()) {
|
||||
return new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
|
||||
}
|
||||
try {
|
||||
return (me.william278.husksync.bukkit.data.DataSerializer.StatisticData) RedisMessage.deserialize(serializedStatisticData);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new IOException("Unable to decode class type.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of a player's statistic data that can be deserialized with {@link #deserializeStatisticData(String)}
|
||||
*
|
||||
* @param player {@link Player} to serialize statistic data of
|
||||
* @return The serialized statistic data as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
public static String getSerializedStatisticData(Player player) throws IOException {
|
||||
HashMap<Statistic, Integer> untypedStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<Material, Integer>> blockStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<Material, Integer>> itemStatisticValues = new HashMap<>();
|
||||
HashMap<Statistic, HashMap<EntityType, Integer>> entityStatisticValues = new HashMap<>();
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
HashMap<Material, Integer> itemValues = new HashMap<>();
|
||||
for (Material itemMaterial : Arrays.stream(Material.values()).filter(Material::isItem).toList()) {
|
||||
itemValues.put(itemMaterial, player.getStatistic(statistic, itemMaterial));
|
||||
}
|
||||
itemStatisticValues.put(statistic, itemValues);
|
||||
}
|
||||
case BLOCK -> {
|
||||
HashMap<Material, Integer> blockValues = new HashMap<>();
|
||||
for (Material blockMaterial : Arrays.stream(Material.values()).filter(Material::isBlock).toList()) {
|
||||
blockValues.put(blockMaterial, player.getStatistic(statistic, blockMaterial));
|
||||
}
|
||||
blockStatisticValues.put(statistic, blockValues);
|
||||
}
|
||||
case ENTITY -> {
|
||||
HashMap<EntityType, Integer> entityValues = new HashMap<>();
|
||||
for (EntityType type : Arrays.stream(EntityType.values()).filter(EntityType::isAlive).toList()) {
|
||||
entityValues.put(type, player.getStatistic(statistic, type));
|
||||
}
|
||||
entityStatisticValues.put(statistic, entityValues);
|
||||
}
|
||||
case UNTYPED -> untypedStatisticValues.put(statistic, player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
|
||||
me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData = new me.william278.husksync.bukkit.data.DataSerializer.StatisticData(untypedStatisticValues, blockStatisticValues, itemStatisticValues, entityStatisticValues);
|
||||
return RedisMessage.serialize(statisticData);
|
||||
}
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
package net.william278.husksync.bukkit.data;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.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, ClassNotFoundException {
|
||||
// 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.serializeInventory(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.PROXY, null, Settings.cluster),
|
||||
RedisMessage.serialize(playerData), Boolean.toString(true))
|
||||
.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, ClassNotFoundException {
|
||||
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, ClassNotFoundException {
|
||||
return switch (inventoryType) {
|
||||
case INVENTORY -> DataSerializer.deserializeInventory(playerData.getSerializedInventory());
|
||||
case ENDER_CHEST -> DataSerializer.deserializeInventory(playerData.getSerializedEnderChest());
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
package net.william278.husksync.bukkit.events;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that will be fired when a {@link Player} has finished being synchronised with the correct {@link PlayerData}.
|
||||
*/
|
||||
public class SyncCompleteEvent extends PlayerEvent {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private final PlayerData data;
|
||||
|
||||
public SyncCompleteEvent(Player player, PlayerData data) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerData} which has just been set on the {@link Player}
|
||||
* @return The {@link PlayerData} that has been set
|
||||
*/
|
||||
public PlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package net.william278.husksync.bukkit.events;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an event that will be fired before a {@link Player} is about to be synchronised with their {@link PlayerData}.
|
||||
*/
|
||||
public class SyncEvent extends PlayerEvent implements Cancellable {
|
||||
|
||||
private boolean cancelled;
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private PlayerData data;
|
||||
|
||||
public SyncEvent(Player player, PlayerData data) {
|
||||
super(player);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link PlayerData} which has just been set on the {@link Player}
|
||||
*
|
||||
* @return The {@link PlayerData} that has been set
|
||||
*/
|
||||
public PlayerData getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the {@link PlayerData} to be synchronised to this player
|
||||
*
|
||||
* @param data The {@link PlayerData} to set to the player
|
||||
*/
|
||||
public void setData(PlayerData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins
|
||||
*
|
||||
* @return true if this event is cancelled
|
||||
*/
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cancellation state of this event. A cancelled event will not be executed in the server, but will still pass to other plugins.
|
||||
*
|
||||
* @param cancel true if you wish to cancel this event
|
||||
*/
|
||||
@Override
|
||||
public void setCancelled(boolean cancel) {
|
||||
this.cancelled = cancel;
|
||||
}
|
||||
}
|
@ -1,166 +0,0 @@
|
||||
package net.william278.husksync.bukkit.listener;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.data.DataViewer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
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 org.bukkit.event.world.WorldSaveEvent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitEventListener implements Listener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||
// 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
|
||||
|
||||
// Update the player's data
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
// Update data to proxy
|
||||
PlayerSetter.updatePlayerData(player, true);
|
||||
|
||||
// Clear player inventory and ender chest
|
||||
player.getInventory().clear();
|
||||
player.getEnderChest().clear();
|
||||
});
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
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();
|
||||
|
||||
// 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())) {
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
try {
|
||||
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 the synchronization timeout retry delay ticks, ensure it will be
|
||||
Bukkit.getScheduler().runTaskLaterAsynchronously(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);
|
||||
}
|
||||
}
|
||||
}, Settings.synchronizationTimeoutRetryDelay);
|
||||
}
|
||||
}
|
||||
|
||||
@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.HIGHEST)
|
||||
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.HIGHEST)
|
||||
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.HIGHEST)
|
||||
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.HIGHEST)
|
||||
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.HIGHEST)
|
||||
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.HIGHEST)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.NORMAL)
|
||||
public void onWorldSave(WorldSaveEvent event) {
|
||||
if (!plugin.isEnabled() || !HuskSyncBukkit.handshakeCompleted) {
|
||||
return;
|
||||
}
|
||||
for (Player playerInWorld : event.getWorld().getPlayers()) {
|
||||
PlayerSetter.updatePlayerData(playerInWorld, false);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
package net.william278.husksync.bukkit.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.config.ConfigLoader;
|
||||
import net.william278.husksync.bukkit.data.DataViewer;
|
||||
import net.william278.husksync.bukkit.migrator.MPDBDeserializer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
public static HashMap<UUID, CompletableFuture<PlayerData>> apiRequests = new HashMap<>();
|
||||
|
||||
// Initialize the listener on the bukkit server
|
||||
public BukkitRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages for proxy servers
|
||||
if (!message.getMessageTarget().targetServerType().equals(Settings.ServerType.BUKKIT)) {
|
||||
return;
|
||||
}
|
||||
// Ignore messages if the plugin is disabled
|
||||
if (!plugin.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
// Ignore messages for other clusters if applicable
|
||||
final String targetClusterId = message.getMessageTarget().targetClusterId();
|
||||
if (targetClusterId != null) {
|
||||
if (!targetClusterId.equalsIgnoreCase(Settings.cluster)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the incoming redis message; either for a specific player or the system
|
||||
if (message.getMessageTarget().targetPlayerUUID() == null) {
|
||||
switch (message.getMessageType()) {
|
||||
case REQUEST_DATA_ON_JOIN -> {
|
||||
UUID playerUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
switch (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];
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
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.PROXY, null, Settings.cluster),
|
||||
RedisMessage.serialize(MPDBDeserializer.convertMPDBData(data)),
|
||||
data.playerName)
|
||||
.send();
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to serialize encoded MPDB data");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
case API_DATA_RETURN -> {
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
if (apiRequests.containsKey(requestUUID)) {
|
||||
try {
|
||||
final PlayerData data = (PlayerData) RedisMessage.deserialize(message.getMessageDataElements()[1]);
|
||||
apiRequests.get(requestUUID).complete(data);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to serialize returned API-requested player data");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
case API_DATA_CANCEL -> {
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
// Cancel requests if no data could be found on the proxy
|
||||
if (apiRequests.containsKey(requestUUID)) {
|
||||
apiRequests.get(requestUUID).cancel(true);
|
||||
}
|
||||
}
|
||||
case RELOAD_CONFIG -> {
|
||||
plugin.reloadConfig();
|
||||
ConfigLoader.loadSettings(plugin.getConfig());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (Player player : Bukkit.getOnlinePlayers()) {
|
||||
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());
|
||||
|
||||
// Set the player's data
|
||||
PlayerSetter.setPlayerFrom(player, data);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling data from the proxy");
|
||||
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(MessageManager.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());
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getLogger().log(level, message);
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
package net.william278.husksync.bukkit.migrator;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.bukkit.util.PlayerSetter;
|
||||
import net.william278.husksync.migrator.MPDBPlayerData;
|
||||
import net.william278.mpdbconverter.MPDBConverter;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.inventory.InventoryType;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.plugin.Plugin;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MPDBDeserializer {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
// Instance of MySqlPlayerDataBridge
|
||||
private static MPDBConverter mpdbConverter;
|
||||
|
||||
public static void setMySqlPlayerDataBridge() {
|
||||
Plugin mpdbPlugin = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||
assert mpdbPlugin != null;
|
||||
mpdbConverter = MPDBConverter.getInstance(mpdbPlugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 contents
|
||||
Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||
if (!mpdbPlayerData.inventoryData.isEmpty() && !mpdbPlayerData.inventoryData.equalsIgnoreCase("none")) {
|
||||
PlayerSetter.setInventory(inventory, mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.inventoryData));
|
||||
}
|
||||
|
||||
// Set armor (if there is data; MPDB stores empty data with literally the word "none". Obviously.)
|
||||
int armorSlot = 36;
|
||||
if (!mpdbPlayerData.armorData.isEmpty() && !mpdbPlayerData.armorData.equalsIgnoreCase("none")) {
|
||||
ItemStack[] armorItems = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.armorData);
|
||||
for (ItemStack armorPiece : armorItems) {
|
||||
if (armorPiece != null) {
|
||||
inventory.setItem(armorSlot, armorPiece);
|
||||
}
|
||||
armorSlot++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Now apply the contents and clear the temporary inventory variable
|
||||
playerData.setSerializedInventory(DataSerializer.serializeInventory(inventory.getContents()));
|
||||
|
||||
// Set ender chest (again, if there is data)
|
||||
ItemStack[] enderChestData;
|
||||
if (!mpdbPlayerData.enderChestData.isEmpty() && !mpdbPlayerData.enderChestData.equalsIgnoreCase("none")) {
|
||||
enderChestData = mpdbConverter.getItemStackFromSerializedData(mpdbPlayerData.enderChestData);
|
||||
} else {
|
||||
enderChestData = new ItemStack[0];
|
||||
}
|
||||
playerData.setSerializedEnderChest(DataSerializer.serializeInventory(enderChestData));
|
||||
|
||||
// Set experience
|
||||
playerData.setExpLevel(mpdbPlayerData.expLevel);
|
||||
playerData.setExpProgress(mpdbPlayerData.expProgress);
|
||||
playerData.setTotalExperience(mpdbPlayerData.totalExperience);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING, "Failed to convert MPDB data to HuskSync's format!");
|
||||
e.printStackTrace();
|
||||
}
|
||||
return playerData;
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
public BukkitUpdateChecker() {
|
||||
super(plugin.getDescription().getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getLogger().log(level, message);
|
||||
}
|
||||
}
|
@ -1,479 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBukkit;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bukkit.events.SyncCompleteEvent;
|
||||
import net.william278.husksync.bukkit.events.SyncEvent;
|
||||
import net.william278.husksync.bukkit.data.DataSerializer;
|
||||
import net.william278.husksync.bukkit.util.nms.AdvancementUtils;
|
||||
import net.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;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class PlayerSetter {
|
||||
|
||||
private static final HuskSyncBukkit plugin = HuskSyncBukkit.getInstance();
|
||||
|
||||
/**
|
||||
* Returns the new serialized PlayerData for a player.
|
||||
*
|
||||
* @param player The {@link Player} to get the new serialized PlayerData for
|
||||
* @return The {@link PlayerData}, serialized as a {@link String}
|
||||
* @throws IOException If the serialization fails
|
||||
*/
|
||||
private static String getNewSerializedPlayerData(Player player) throws IOException {
|
||||
final double maxHealth = getMaxHealth(player); // Get the player's max health (used to determine health as well)
|
||||
return RedisMessage.serialize(new PlayerData(player.getUniqueId(),
|
||||
DataSerializer.serializeInventory(player.getInventory().getContents()),
|
||||
DataSerializer.serializeInventory(player.getEnderChest().getContents()),
|
||||
Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0D,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
DataSerializer.serializePotionEffects(getPlayerPotionEffects(player)),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().toString(),
|
||||
DataSerializer.getSerializedStatisticData(player),
|
||||
player.isFlying(),
|
||||
DataSerializer.getSerializedAdvancements(player),
|
||||
DataSerializer.getSerializedLocation(player)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s maximum health, minus any health boost effects
|
||||
*
|
||||
* @param player The {@link Player} to get the maximum health of
|
||||
* @return The {@link Player}'s max health
|
||||
*/
|
||||
private static double getMaxHealth(Player player) {
|
||||
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
|
||||
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
|
||||
assert healthBoostEffect != null;
|
||||
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
|
||||
maxHealth -= healthBoostBonus;
|
||||
}
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s active potion effects in a {@link PotionEffect} array
|
||||
*
|
||||
* @param player The {@link Player} to get the effects of
|
||||
* @return The {@link PotionEffect} array
|
||||
*/
|
||||
private static PotionEffect[] getPlayerPotionEffects(Player player) {
|
||||
PotionEffect[] potionEffects = new PotionEffect[player.getActivePotionEffects().size()];
|
||||
int arrayIndex = 0;
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
potionEffects[arrayIndex] = effect;
|
||||
arrayIndex++;
|
||||
}
|
||||
return potionEffects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a {@link Player}'s data, sending it to the proxy
|
||||
*
|
||||
* @param player {@link Player} to send data to proxy
|
||||
* @param bounceBack whether the plugin should bounce-back the updated data to the player (used for server switching)
|
||||
*/
|
||||
public static void updatePlayerData(Player player, boolean bounceBack) {
|
||||
// Send a redis message with the player's last updated PlayerData version UUID and their new PlayerData
|
||||
try {
|
||||
final String serializedPlayerData = getNewSerializedPlayerData(player);
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_UPDATE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
serializedPlayerData, Boolean.toString(bounceBack)).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to send a PlayerData update to the proxy", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request a {@link Player}'s data from the proxy
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the {@link Player} to fetch PlayerData from
|
||||
* @throws IOException If the request Redis message data fails to serialize
|
||||
*/
|
||||
public static void requestPlayerData(UUID playerUUID) throws IOException {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_REQUEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.PROXY, null, Settings.cluster),
|
||||
playerUUID.toString()).send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player from their PlayerData, based on settings
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param dataToSet The {@link PlayerData} to assign to the player
|
||||
*/
|
||||
public static void setPlayerFrom(Player player, PlayerData dataToSet) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
// Handle the SyncEvent
|
||||
SyncEvent syncEvent = new SyncEvent(player, dataToSet);
|
||||
Bukkit.getPluginManager().callEvent(syncEvent);
|
||||
final PlayerData data = syncEvent.getData();
|
||||
if (syncEvent.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the data is flagged as being default data, skip setting
|
||||
if (data.useDefaultData) {
|
||||
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
|
||||
try {
|
||||
if (Settings.syncAdvancements) {
|
||||
List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords
|
||||
= DataSerializer.deserializeAdvancementData(data.getSerializedAdvancements());
|
||||
|
||||
if (Settings.useNativeImplementation) {
|
||||
try {
|
||||
nativeSyncPlayerAdvancements(player, advancementRecords);
|
||||
} catch (Exception e) {
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Your server does not support a native implementation of achievements synchronization");
|
||||
plugin.getLogger().log(Level.WARNING,
|
||||
"Your server version is {0}. Please disable using native implementation!", Bukkit.getVersion());
|
||||
|
||||
Settings.useNativeImplementation = false;
|
||||
setPlayerAdvancements(player, advancementRecords, data);
|
||||
plugin.getLogger().log(Level.SEVERE, e.getMessage(), e);
|
||||
}
|
||||
} else {
|
||||
setPlayerAdvancements(player, advancementRecords, data);
|
||||
}
|
||||
}
|
||||
if (Settings.syncInventories) {
|
||||
setPlayerInventory(player, DataSerializer.deserializeInventory(data.getSerializedInventory()));
|
||||
player.getInventory().setHeldItemSlot(data.getSelectedSlot());
|
||||
}
|
||||
if (Settings.syncEnderChests) {
|
||||
setPlayerEnderChest(player, DataSerializer.deserializeInventory(data.getSerializedEnderChest()));
|
||||
}
|
||||
if (Settings.syncHealth) {
|
||||
setPlayerHealth(player, data.getHealth(), data.getMaxHealth(), data.getHealthScale());
|
||||
}
|
||||
if (Settings.syncHunger) {
|
||||
player.setFoodLevel(data.getHunger());
|
||||
player.setSaturation(data.getSaturation());
|
||||
player.setExhaustion(data.getSaturationExhaustion());
|
||||
}
|
||||
if (Settings.syncExperience) {
|
||||
// This is also handled when syncing advancements to ensure its correct
|
||||
setPlayerExperience(player, data);
|
||||
}
|
||||
if (Settings.syncPotionEffects) {
|
||||
setPlayerPotionEffects(player, DataSerializer.deserializePotionEffects(data.getSerializedEffectData()));
|
||||
}
|
||||
if (Settings.syncStatistics) {
|
||||
setPlayerStatistics(player, DataSerializer.deserializeStatisticData(data.getSerializedStatistics()));
|
||||
}
|
||||
if (Settings.syncGameMode) {
|
||||
player.setGameMode(GameMode.valueOf(data.getGameMode()));
|
||||
}
|
||||
if (Settings.syncLocation) {
|
||||
setPlayerLocation(player, DataSerializer.deserializePlayerLocationData(data.getSerializedLocation()));
|
||||
}
|
||||
if (Settings.syncFlight) {
|
||||
if (data.isFlying()) {
|
||||
player.setAllowFlight(true);
|
||||
}
|
||||
player.setFlying(player.getAllowFlight() && data.isFlying());
|
||||
}
|
||||
|
||||
// Handle the SyncCompleteEvent
|
||||
Bukkit.getPluginManager().callEvent(new SyncCompleteEvent(player, data));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
plugin.getLogger().log(Level.SEVERE, "Failed to deserialize PlayerData", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's ender chest from a set of {@link ItemStack}s
|
||||
*
|
||||
* @param player The player to set the inventory of
|
||||
* @param items The array of {@link ItemStack}s to set
|
||||
*/
|
||||
private static void setPlayerEnderChest(Player player, ItemStack[] items) {
|
||||
setInventory(player.getEnderChest(), items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a player's inventory from a set of {@link ItemStack}s
|
||||
*
|
||||
* @param player The player to set the inventory of
|
||||
* @param items The array of {@link ItemStack}s to set
|
||||
*/
|
||||
private static void setPlayerInventory(Player player, ItemStack[] items) {
|
||||
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) {
|
||||
inventory.setItem(index, item);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's current potion effects from a set of {@link PotionEffect[]}
|
||||
*
|
||||
* @param player The player to set the potion effects of
|
||||
* @param effects The array of {@link PotionEffect}s to set
|
||||
*/
|
||||
private static void setPlayerPotionEffects(Player player, PotionEffect[] effects) {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
}
|
||||
|
||||
private static void nativeSyncPlayerAdvancements(final Player player, final List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementRecords) {
|
||||
final Object playerAdvancements = AdvancementUtils.getPlayerAdvancements(player);
|
||||
|
||||
// Clear
|
||||
AdvancementUtils.clearPlayerAdvancements(playerAdvancements);
|
||||
AdvancementUtils.clearVisibleAdvancements(playerAdvancements);
|
||||
|
||||
advancementRecords.forEach(advancementRecord -> {
|
||||
NamespacedKey namespacedKey = Objects.requireNonNull(
|
||||
NamespacedKey.fromString(advancementRecord.key()),
|
||||
"Invalid Namespaced key of " + advancementRecord.key()
|
||||
);
|
||||
|
||||
Advancement bukkitAdvancement = Bukkit.getAdvancement(namespacedKey);
|
||||
if (bukkitAdvancement == null) {
|
||||
plugin.getLogger().log(Level.WARNING, "Ignored advancement '{0}' - it doesn't exist anymore?", namespacedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
Object advancement = AdvancementUtils.getHandle(bukkitAdvancement);
|
||||
Map<String, Date> criteriaList = advancementRecord.criteriaMap();
|
||||
{
|
||||
Map<String, Object> nativeCriteriaMap = new HashMap<>();
|
||||
criteriaList.forEach((criteria, date) ->
|
||||
nativeCriteriaMap.put(criteria, AdvancementUtils.newCriterionProgress(date))
|
||||
);
|
||||
Object nativeAdvancementProgress = AdvancementUtils.newAdvancementProgress(nativeCriteriaMap);
|
||||
|
||||
AdvancementUtils.startProgress(playerAdvancements, advancement, nativeAdvancementProgress);
|
||||
}
|
||||
});
|
||||
AdvancementUtils.ensureAllVisible(playerAdvancements); // Set all completed advancement is visible
|
||||
AdvancementUtils.markPlayerAdvancementsFirst(playerAdvancements); // Mark the sending of visible advancement as the first
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a player's advancements and progress to match the advancementData
|
||||
*
|
||||
* @param player The player to set the advancements of
|
||||
* @param advancementData The ArrayList of {@link me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate}s to set
|
||||
*/
|
||||
private static void setPlayerAdvancements(Player player, List<me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate> advancementData, PlayerData data) {
|
||||
// Temporarily disable advancement announcing if needed
|
||||
boolean announceAdvancementUpdate = false;
|
||||
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
announceAdvancementUpdate = true;
|
||||
}
|
||||
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
|
||||
|
||||
// Run async because advancement loading is very slow
|
||||
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
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);
|
||||
for (me.william278.husksync.bukkit.data.DataSerializer.AdvancementRecordDate record : advancementData) {
|
||||
// If the advancement is one on the data
|
||||
if (record.key().equals(advancement.getKey().getNamespace() + ":" + advancement.getKey().getKey())) {
|
||||
|
||||
// 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.criteriaMap().keySet()) {
|
||||
if (!playerProgress.getAwardedCriteria().contains(awardCriteria)) {
|
||||
Bukkit.getScheduler().runTask(plugin, () -> player.getAdvancementProgress(advancement).awardCriteria(awardCriteria));
|
||||
correctExperienceCheck = true;
|
||||
}
|
||||
currentlyAwardedCriteria.remove(awardCriteria);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperienceCheck) {
|
||||
if (Settings.syncExperience) {
|
||||
setPlayerExperience(player, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(plugin, () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a player's statistics (in the Statistic menu)
|
||||
*
|
||||
* @param player The player to set the statistics of
|
||||
* @param statisticData The {@link me.william278.husksync.bukkit.data.DataSerializer.StatisticData} to set
|
||||
*/
|
||||
private static void setPlayerStatistics(Player player, me.william278.husksync.bukkit.data.DataSerializer.StatisticData statisticData) {
|
||||
// Set untyped statistics
|
||||
for (Statistic statistic : statisticData.untypedStatisticValues().keySet()) {
|
||||
player.setStatistic(statistic, statisticData.untypedStatisticValues().get(statistic));
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (Statistic statistic : statisticData.blockStatisticValues().keySet()) {
|
||||
for (Material blockMaterial : statisticData.blockStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, blockMaterial, statisticData.blockStatisticValues().get(statistic).get(blockMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (Statistic statistic : statisticData.itemStatisticValues().keySet()) {
|
||||
for (Material itemMaterial : statisticData.itemStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, itemMaterial, statisticData.itemStatisticValues().get(statistic).get(itemMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (Statistic statistic : statisticData.entityStatisticValues().keySet()) {
|
||||
for (EntityType entityType : statisticData.entityStatisticValues().get(statistic).keySet()) {
|
||||
player.setStatistic(statistic, entityType, statisticData.entityStatisticValues().get(statistic).get(entityType));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation} data
|
||||
*
|
||||
* @param player The {@link Player} to teleport
|
||||
* @param location The {@link me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation}
|
||||
*/
|
||||
private static void setPlayerLocation(Player player, me.william278.husksync.bukkit.data.DataSerializer.PlayerLocation location) {
|
||||
// Don't teleport if the location is invalid
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the world; if the names match, use that
|
||||
World world = Bukkit.getWorld(location.worldName());
|
||||
if (world == null) {
|
||||
|
||||
// If the names don't match, find the corresponding world with the same dimension environment
|
||||
for (World worldOnServer : Bukkit.getWorlds()) {
|
||||
if (worldOnServer.getEnvironment().equals(location.environment())) {
|
||||
world = worldOnServer;
|
||||
}
|
||||
}
|
||||
|
||||
// If that still fails, return
|
||||
if (world == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Teleport the player
|
||||
player.teleport(new Location(world, location.x(), location.y(), location.z(), location.yaw(), location.pitch()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Correctly set a {@link Player}'s health data
|
||||
*
|
||||
* @param player The {@link Player} to set
|
||||
* @param health Health to set to the player
|
||||
* @param maxHealth Max health to set to the player
|
||||
* @param healthScale Health scaling to apply to the player
|
||||
*/
|
||||
private static void setPlayerHealth(Player player, double health, double maxHealth, double healthScale) {
|
||||
// Set max health
|
||||
if (maxHealth != 0D) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).setBaseValue(maxHealth);
|
||||
}
|
||||
|
||||
// Set health
|
||||
double currentHealth = player.getHealth();
|
||||
if (health != currentHealth) player.setHealth(currentHealth > maxHealth ? maxHealth : health);
|
||||
|
||||
// Set health scaling if needed
|
||||
if (healthScale != 0D) {
|
||||
player.setHealthScale(healthScale);
|
||||
} else {
|
||||
player.setHealthScale(maxHealth);
|
||||
}
|
||||
player.setHealthScaled(healthScale != 0D);
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class AdvancementUtils {
|
||||
|
||||
public final static Class<?> PLAYER_ADVANCEMENT;
|
||||
private final static Field PLAYER_ADVANCEMENTS_MAP;
|
||||
private final static Field PLAYER_VISIBLE_SET;
|
||||
private final static Field PLAYER_ADVANCEMENTS;
|
||||
private final static Field CRITERIA_MAP;
|
||||
private final static Field CRITERIA_DATE;
|
||||
private final static Field IS_FIRST_PACKET;
|
||||
private final static Method GET_HANDLE;
|
||||
private final static Method START_PROGRESS;
|
||||
private final static Method ENSURE_ALL_VISIBLE;
|
||||
private final static Class<?> ADVANCEMENT_PROGRESS;
|
||||
private final static Class<?> CRITERION_PROGRESS;
|
||||
|
||||
static {
|
||||
Class<?> SERVER_PLAYER = MinecraftVersionUtils.getMinecraftClass("level.EntityPlayer");
|
||||
PLAYER_ADVANCEMENTS = ThrowSupplier.get(() -> SERVER_PLAYER.getDeclaredField("cs"));
|
||||
PLAYER_ADVANCEMENTS.setAccessible(true);
|
||||
|
||||
Class<?> CRAFT_ADVANCEMENT = MinecraftVersionUtils.getBukkitClass("advancement.CraftAdvancement");
|
||||
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ADVANCEMENT.getDeclaredMethod("getHandle"));
|
||||
|
||||
ADVANCEMENT_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.AdvancementProgress"));
|
||||
CRITERIA_MAP = ThrowSupplier.get(() -> ADVANCEMENT_PROGRESS.getDeclaredField("a"));
|
||||
CRITERIA_MAP.setAccessible(true);
|
||||
|
||||
CRITERION_PROGRESS = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.CriterionProgress"));
|
||||
CRITERIA_DATE = ThrowSupplier.get(() -> CRITERION_PROGRESS.getDeclaredField("b"));
|
||||
CRITERIA_DATE.setAccessible(true);
|
||||
|
||||
Class<?> ADVANCEMENT = ThrowSupplier.get(() -> Class.forName("net.minecraft.advancements.Advancement"));
|
||||
|
||||
PLAYER_ADVANCEMENT = MinecraftVersionUtils.getMinecraftClass("AdvancementDataPlayer");
|
||||
PLAYER_ADVANCEMENTS_MAP = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("h"));
|
||||
PLAYER_ADVANCEMENTS_MAP.setAccessible(true);
|
||||
|
||||
PLAYER_VISIBLE_SET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("i"));
|
||||
PLAYER_VISIBLE_SET.setAccessible(true);
|
||||
|
||||
START_PROGRESS = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("a", ADVANCEMENT, ADVANCEMENT_PROGRESS));
|
||||
START_PROGRESS.setAccessible(true);
|
||||
|
||||
ENSURE_ALL_VISIBLE = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredMethod("c"));
|
||||
ENSURE_ALL_VISIBLE.setAccessible(true);
|
||||
|
||||
IS_FIRST_PACKET = ThrowSupplier.get(() -> PLAYER_ADVANCEMENT.getDeclaredField("n"));
|
||||
IS_FIRST_PACKET.setAccessible(true);
|
||||
}
|
||||
|
||||
public static void markPlayerAdvancementsFirst(final Object playerAdvancements) {
|
||||
try {
|
||||
IS_FIRST_PACKET.set(playerAdvancements, true);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getPlayerAdvancements(Player player) {
|
||||
Object nativePlayer = EntityUtils.getHandle(player);
|
||||
try {
|
||||
return PLAYER_ADVANCEMENTS.get(nativePlayer);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearPlayerAdvancements(final Object playerAdvancement) {
|
||||
try {
|
||||
((Map<?, ?>) PLAYER_ADVANCEMENTS_MAP.get(playerAdvancement))
|
||||
.clear();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getHandle(Advancement advancement) {
|
||||
try {
|
||||
return GET_HANDLE.invoke(advancement);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object newCriterionProgress(final Date date) {
|
||||
try {
|
||||
Object nativeCriterionProgress = CRITERION_PROGRESS.getDeclaredConstructor().newInstance();
|
||||
CRITERIA_DATE.set(nativeCriterionProgress, date);
|
||||
return nativeCriterionProgress;
|
||||
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked") // Suppress unchecked cast warnings here
|
||||
public static Object newAdvancementProgress(final Map<String, Object> criteria) {
|
||||
try {
|
||||
Object nativeAdvancementProgress = ADVANCEMENT_PROGRESS.getDeclaredConstructor().newInstance();
|
||||
|
||||
final Map<String, Object> criteriaMap = (Map<String, Object>) CRITERIA_MAP.get(nativeAdvancementProgress);
|
||||
criteriaMap.putAll(criteria);
|
||||
|
||||
return nativeAdvancementProgress;
|
||||
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void startProgress(final Object playerAdvancements, final Object advancement, final Object advancementProgress) {
|
||||
try {
|
||||
START_PROGRESS.invoke(playerAdvancements, advancement, advancementProgress);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void ensureAllVisible(final Object playerAdvancements) {
|
||||
try {
|
||||
ENSURE_ALL_VISIBLE.invoke(playerAdvancements);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void clearVisibleAdvancements(final Object playerAdvancements) {
|
||||
try {
|
||||
((Set<?>) PLAYER_VISIBLE_SET.get(playerAdvancements))
|
||||
.clear();
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import org.bukkit.entity.LivingEntity;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class EntityUtils {
|
||||
|
||||
private final static Method GET_HANDLE;
|
||||
|
||||
static {
|
||||
final Class<?> CRAFT_ENTITY = MinecraftVersionUtils.getBukkitClass("entity.CraftEntity");
|
||||
GET_HANDLE = ThrowSupplier.get(() -> CRAFT_ENTITY.getDeclaredMethod("getHandle"));
|
||||
}
|
||||
|
||||
public static Object getHandle(LivingEntity livingEntity) throws RuntimeException {
|
||||
try {
|
||||
return GET_HANDLE.invoke(livingEntity);
|
||||
} catch (IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package net.william278.husksync.bukkit.util.nms;
|
||||
|
||||
import net.william278.husksync.util.ThrowSupplier;
|
||||
import net.william278.husksync.util.VersionUtils;
|
||||
import org.bukkit.Bukkit;
|
||||
|
||||
public class MinecraftVersionUtils {
|
||||
|
||||
public final static String CRAFTBUKKIT_PACKAGE_PATH = Bukkit.getServer().getClass().getPackage().getName();
|
||||
|
||||
public final static String PACKAGE_VERSION = CRAFTBUKKIT_PACKAGE_PATH.split("\\.")[3];
|
||||
public final static VersionUtils.Version SERVER_VERSION
|
||||
= VersionUtils.Version.of(Bukkit.getBukkitVersion().split("-")[0]);
|
||||
public final static String MINECRAFT_PACKAGE = SERVER_VERSION.compareTo(VersionUtils.Version.of("1.17")) < 0 ?
|
||||
"net.minecraft.server.".concat(PACKAGE_VERSION) : "net.minecraft.server";
|
||||
|
||||
public static Class<?> getBukkitClass(String path) {
|
||||
return ThrowSupplier.get(() -> Class.forName(CRAFTBUKKIT_PACKAGE_PATH.concat(".").concat(path)));
|
||||
}
|
||||
|
||||
public static Class<?> getMinecraftClass(String path) {
|
||||
return ThrowSupplier.get(() -> Class.forName(MINECRAFT_PACKAGE.concat(".").concat(path)));
|
||||
}
|
||||
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
redis_settings:
|
||||
host: 'localhost'
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
synchronisation_settings:
|
||||
inventories: true
|
||||
ender_chests: true
|
||||
health: true
|
||||
hunger: true
|
||||
experience: true
|
||||
potion_effects: true
|
||||
statistics: true
|
||||
game_mode: true
|
||||
advancements: true
|
||||
location: false
|
||||
flight: false
|
||||
cluster_id: 'main'
|
||||
check_for_updates: true
|
||||
synchronization_timeout_retry_delay: 15
|
||||
save_on_world_save: true
|
||||
native_advancement_synchronization: false
|
@ -1,171 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.byteflux.libby.BungeeLibraryManager;
|
||||
import net.byteflux.libby.Library;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.plugin.Plugin;
|
||||
import net.william278.husksync.bungeecord.command.BungeeCommand;
|
||||
import net.william278.husksync.bungeecord.config.ConfigLoader;
|
||||
import net.william278.husksync.bungeecord.config.ConfigManager;
|
||||
import net.william278.husksync.bungeecord.listener.BungeeEventListener;
|
||||
import net.william278.husksync.bungeecord.listener.BungeeRedisListener;
|
||||
import net.william278.husksync.bungeecord.util.BungeeLogger;
|
||||
import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.data.DataManager;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import org.bstats.bungeecord.Metrics;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class HuskSyncBungeeCord extends Plugin {
|
||||
|
||||
// BungeeCord bStats ID (different to Bukkit)
|
||||
private static final int METRICS_ID = 13141;
|
||||
|
||||
private static HuskSyncBungeeCord instance;
|
||||
|
||||
public static HuskSyncBungeeCord getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Whether the plugin is ready to accept redis messages
|
||||
public static boolean readyForRedis = false;
|
||||
|
||||
// Whether the plugin is in the process of disabling and should skip responding to handshake confirmations
|
||||
public static boolean isDisabling = false;
|
||||
|
||||
/**
|
||||
* Set of all the {@link Server}s that have completed the synchronisation handshake with HuskSync on the proxy
|
||||
*/
|
||||
public static HashSet<Server> synchronisedServers;
|
||||
|
||||
public static DataManager dataManager;
|
||||
|
||||
public static MPDBMigrator mpdbMigrator;
|
||||
|
||||
public static BungeeRedisListener redisListener;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
public Logger getBungeeLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
logger = new BungeeLogger(getLogger());
|
||||
fetchDependencies();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Plugin startup logic
|
||||
synchronisedServers = new HashSet<>();
|
||||
|
||||
// Load config
|
||||
ConfigManager.loadConfig();
|
||||
|
||||
// Load settings from config
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
// Load messages
|
||||
ConfigManager.loadMessages();
|
||||
|
||||
// Load locales from messages
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new BungeeUpdateChecker(getDescription().getVersion()).logToConsole();
|
||||
}
|
||||
|
||||
// Setup data manager
|
||||
dataManager = new DataManager(getBungeeLogger(), getDataFolder());
|
||||
|
||||
// Ensure the data manager initialized correctly
|
||||
if (dataManager.hasFailedInitialization) {
|
||||
getBungeeLogger().severe("Failed to initialize the HuskSync database(s).\n" +
|
||||
"HuskSync will now abort loading itself (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
// Setup player data cache
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
dataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
|
||||
}
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new BungeeRedisListener();
|
||||
|
||||
// Register listener
|
||||
getProxy().getPluginManager().registerListener(this, new BungeeEventListener());
|
||||
|
||||
// Register command
|
||||
getProxy().getPluginManager().registerCommand(this, new BungeeCommand());
|
||||
|
||||
// Prepare the migrator for use if needed
|
||||
mpdbMigrator = new MPDBMigrator(getBungeeLogger());
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
new Metrics(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getBungeeLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getBungeeLogger().info("Enabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
|
||||
// Mark as ready for redis message processing
|
||||
readyForRedis = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
// Plugin shutdown logic
|
||||
isDisabling = true;
|
||||
|
||||
// Send terminating handshake message
|
||||
for (Server server : synchronisedServers) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
|
||||
server.serverUUID().toString(),
|
||||
ProxyServer.getInstance().getName()).send();
|
||||
} catch (IOException e) {
|
||||
getBungeeLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
dataManager.closeDatabases();
|
||||
|
||||
// Log to console
|
||||
getBungeeLogger().info("Disabled HuskSync (" + getProxy().getName() + ") v" + getDescription().getVersion());
|
||||
}
|
||||
|
||||
// Load dependencies
|
||||
private void fetchDependencies() {
|
||||
BungeeLibraryManager manager = new BungeeLibraryManager(getInstance());
|
||||
|
||||
Library mySqlLib = Library.builder()
|
||||
.groupId("mysql")
|
||||
.artifactId("mysql-connector-java")
|
||||
.version("8.0.29")
|
||||
.build();
|
||||
|
||||
Library sqLiteLib = Library.builder()
|
||||
.groupId("org.xerial")
|
||||
.artifactId("sqlite-jdbc")
|
||||
.version("3.36.0.3")
|
||||
.build();
|
||||
|
||||
manager.addMavenCentral();
|
||||
manager.loadLibrary(mySqlLib);
|
||||
manager.loadLibrary(sqLiteLib);
|
||||
}
|
||||
}
|
@ -1,424 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.bungeecord.config.ConfigLoader;
|
||||
import net.william278.husksync.bungeecord.config.ConfigManager;
|
||||
import net.william278.husksync.bungeecord.util.BungeeUpdateChecker;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.command.HuskSyncCommand;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.md_5.bungee.api.CommandSender;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
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.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BungeeCommand extends Command implements TabExecutor, HuskSyncCommand {
|
||||
|
||||
private final static HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public BungeeCommand() {
|
||||
super("husksync", null, "hs");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (HuskSyncBungeeCord.synchronisedServers.size() == 0) {
|
||||
player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (args.length >= 1) {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "about", "info" -> sendAboutInformation(player);
|
||||
case "update" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent());
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Check Bukkit servers needing updates
|
||||
int updatesNeeded = 0;
|
||||
String bukkitBrand = "Spigot";
|
||||
String bukkitVersion = "1.0";
|
||||
for (Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||
BungeeUpdateChecker updateChecker = new BungeeUpdateChecker(server.huskSyncVersion());
|
||||
if (!updateChecker.isUpToDate()) {
|
||||
updatesNeeded++;
|
||||
bukkitBrand = server.serverBrand();
|
||||
bukkitVersion = server.huskSyncVersion();
|
||||
}
|
||||
}
|
||||
|
||||
// Check Bungee servers needing updates and send message
|
||||
BungeeUpdateChecker proxyUpdateChecker = new BungeeUpdateChecker(plugin.getDescription().getVersion());
|
||||
if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent());
|
||||
if (!proxyUpdateChecker.isUpToDate()) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on the " + ProxyServer.getInstance().getName() + " proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
if (updatesNeeded > 0) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent());
|
||||
}
|
||||
});
|
||||
}
|
||||
case "invsee", "openinv", "inventory" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openInventory(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync invsee <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "echest", "enderchest" -> {
|
||||
if (!player.hasPermission("husksync.command.ender_chest")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openEnderChest(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
|
||||
.replaceAll("%1%", "/husksync echest <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only")
|
||||
.replaceAll("%1%", ProxyServer.getInstance().getName())).toComponent());
|
||||
}
|
||||
case "status" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
int playerDataSize = 0;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
playerDataSize += HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).playerData.size();
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
|
||||
.replaceAll("%1%", String.valueOf(HuskSyncBungeeCord.synchronisedServers.size()))
|
||||
.replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
ConfigManager.loadConfig();
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
ConfigManager.loadMessages();
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Send reload request to all bukkit servers
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
"reload")
|
||||
.send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize reload notification message data");
|
||||
}
|
||||
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent());
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync <about/status/invsee/echest>")).toComponent());
|
||||
}
|
||||
} else {
|
||||
sendAboutInformation(player);
|
||||
}
|
||||
} else {
|
||||
// Database migration wizard
|
||||
if (args.length >= 1) {
|
||||
if (args[0].equalsIgnoreCase("migrate")) {
|
||||
MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
|
||||
if (args.length == 1) {
|
||||
sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
This will migrate data from the MySQLPlayerDataBridge
|
||||
plugin to HuskSync.
|
||||
|
||||
Data that will be migrated:
|
||||
- Inventories
|
||||
- Ender Chests
|
||||
- Experience points
|
||||
|
||||
Other non-vital data, such as current health, hunger
|
||||
& potion effects will not be migrated to ensure that
|
||||
migration does not take an excessive amount of time.
|
||||
|
||||
To do this, you need to have MySqlPlayerDataBridge
|
||||
and HuskSync installed on one Spigot server as well
|
||||
as HuskSync installed on the proxy (which you have)
|
||||
|
||||
>To proceed, type: husksync migrate setup""").toComponent());
|
||||
} else {
|
||||
switch (args[1].toLowerCase()) {
|
||||
case "setup" -> sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
The following database settings will be used.
|
||||
Please make sure they match the correct settings to
|
||||
access your MySQLPlayerDataBridge Data
|
||||
|
||||
sourceHost: %1%
|
||||
sourcePort: %2%
|
||||
sourceDatabase: %3%
|
||||
sourceUsername: %4%
|
||||
sourcePassword: %5%
|
||||
|
||||
sourceInventoryTableName: %6%
|
||||
sourceEnderChestTableName: %7%
|
||||
sourceExperienceTableName: %8%
|
||||
|
||||
targetCluster: %9%
|
||||
|
||||
To change a setting, type:
|
||||
husksync migrate setting <settingName> <value>
|
||||
|
||||
Please ensure no players are logged in to the network
|
||||
and that at least one Spigot server is online with
|
||||
both HuskSync AND MySqlPlayerDataBridge installed AND
|
||||
that the server has been configured with the correct
|
||||
Redis credentials.
|
||||
|
||||
Warning: Data will be saved to your configured data
|
||||
source, which is currently a %10% database.
|
||||
Please make sure you are happy with this, or stop
|
||||
the proxy server and edit this in config.yml
|
||||
|
||||
Warning: Migration will overwrite any current data
|
||||
saved by HuskSync. It will not, however, delete any
|
||||
data from the source MySQLPlayerDataBridge database.
|
||||
|
||||
>When done, type: husksync migrate start"""
|
||||
.replaceAll("%1%", migrator.migrationSettings.sourceHost)
|
||||
.replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort))
|
||||
.replaceAll("%3%", migrator.migrationSettings.sourceDatabase)
|
||||
.replaceAll("%4%", migrator.migrationSettings.sourceUsername)
|
||||
.replaceAll("%5%", migrator.migrationSettings.sourcePassword)
|
||||
.replaceAll("%6%", migrator.migrationSettings.inventoryDataTable)
|
||||
.replaceAll("%7%", migrator.migrationSettings.enderChestDataTable)
|
||||
.replaceAll("%8%", migrator.migrationSettings.expDataTable)
|
||||
.replaceAll("%9%", migrator.migrationSettings.targetCluster)
|
||||
.replaceAll("%10%", Settings.dataStorageType.toString())
|
||||
).toComponent());
|
||||
case "setting" -> {
|
||||
if (args.length == 4) {
|
||||
String value = args[3];
|
||||
switch (args[2]) {
|
||||
case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value;
|
||||
case "sourcePort", "port" -> {
|
||||
try {
|
||||
migrator.migrationSettings.sourcePort = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value;
|
||||
case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value;
|
||||
case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value;
|
||||
case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value;
|
||||
case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value;
|
||||
case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value;
|
||||
case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value;
|
||||
default -> {
|
||||
sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting <settingName> <value>").toComponent());
|
||||
}
|
||||
}
|
||||
case "start" -> {
|
||||
sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent());
|
||||
|
||||
// If the migrator is ready, execute the migration asynchronously
|
||||
if (HuskSyncBungeeCord.mpdbMigrator.readyToMigrate(ProxyServer.getInstance().getOnlineCount(),
|
||||
HuskSyncBungeeCord.synchronisedServers)) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () ->
|
||||
HuskSyncBungeeCord.mpdbMigrator.executeMigrationOperations(HuskSyncBungeeCord.dataManager,
|
||||
HuskSyncBungeeCord.synchronisedServers, HuskSyncBungeeCord.redisListener));
|
||||
}
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate <args>").toComponent());
|
||||
}
|
||||
}
|
||||
|
||||
// View the inventory of a player specified by their name
|
||||
private void openInventory(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
});
|
||||
}
|
||||
|
||||
// View the ender chest of a player specified by their name
|
||||
public void openEnderChest(ProxiedPlayer viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getName().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (ProxyServer.getInstance().getPlayer(targetPlayerName) != null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncBungeeCord.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(), null),
|
||||
plugin.getProxy().getName(), plugin.getDescription().getVersion()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Tab completion
|
||||
@Override
|
||||
public Iterable<String> onTabComplete(CommandSender sender, String[] args) {
|
||||
if (sender instanceof ProxiedPlayer player) {
|
||||
if (args.length == 1) {
|
||||
final ArrayList<String> subCommands = new ArrayList<>();
|
||||
for (SubCommand subCommand : SUB_COMMANDS) {
|
||||
if (subCommand.permission() != null) {
|
||||
if (!player.hasPermission(subCommand.permission())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
subCommands.add(subCommand.command());
|
||||
}
|
||||
// Automatically filter the sub commands' order in tab completion by what the player has typed
|
||||
return subCommands.stream().filter(val -> val.startsWith(args[0]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
private static Configuration copyDefaults(Configuration config) {
|
||||
// Get the config version and update if needed
|
||||
String configVersion = config.getString("config_file_version", "1.0");
|
||||
if (configVersion.contains("-dev")) {
|
||||
configVersion = configVersion.replaceAll("-dev", "");
|
||||
}
|
||||
if (!configVersion.equals(plugin.getDescription().getVersion())) {
|
||||
if (configVersion.equalsIgnoreCase("1.0")) {
|
||||
config.set("check_for_updates", true);
|
||||
}
|
||||
if (configVersion.equalsIgnoreCase("1.0") || configVersion.equalsIgnoreCase("1.0.1") || configVersion.equalsIgnoreCase("1.0.2") || configVersion.equalsIgnoreCase("1.0.3")) {
|
||||
config.set("clusters.main.player_table", "husksync_players");
|
||||
config.set("clusters.main.data_table", "husksync_data");
|
||||
}
|
||||
config.set("config_file_version", plugin.getDescription().getVersion());
|
||||
}
|
||||
// Save the config back
|
||||
ConfigManager.saveConfig(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public static void loadSettings(Configuration loadedConfig) throws IllegalArgumentException {
|
||||
Configuration config = copyDefaults(loadedConfig);
|
||||
|
||||
Settings.language = config.getString("language", "en-gb");
|
||||
|
||||
Settings.serverType = Settings.ServerType.PROXY;
|
||||
Settings.automaticUpdateChecks = config.getBoolean("check_for_updates", true);
|
||||
Settings.redisHost = config.getString("redis_settings.host", "localhost");
|
||||
Settings.redisPort = config.getInt("redis_settings.port", 6379);
|
||||
Settings.redisPassword = config.getString("redis_settings.password", "");
|
||||
Settings.redisSSL = config.getBoolean("redis_settings.use_ssl", false);
|
||||
|
||||
Settings.dataStorageType = Settings.DataStorageType.valueOf(config.getString("data_storage_settings.database_type", "sqlite").toUpperCase());
|
||||
if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
|
||||
Settings.mySQLHost = config.getString("data_storage_settings.mysql_settings.host", "localhost");
|
||||
Settings.mySQLPort = config.getInt("data_storage_settings.mysql_settings.port", 3306);
|
||||
Settings.mySQLDatabase = config.getString("data_storage_settings.mysql_settings.database", "HuskSync");
|
||||
Settings.mySQLUsername = config.getString("data_storage_settings.mysql_settings.username", "root");
|
||||
Settings.mySQLPassword = config.getString("data_storage_settings.mysql_settings.password", "pa55w0rd");
|
||||
Settings.mySQLParams = config.getString("data_storage_settings.mysql_settings.params", "?autoReconnect=true&useSSL=false");
|
||||
}
|
||||
|
||||
Settings.hikariMaximumPoolSize = config.getInt("data_storage_settings.hikari_pool_settings.maximum_pool_size", 10);
|
||||
Settings.hikariMinimumIdle = config.getInt("data_storage_settings.hikari_pool_settings.minimum_idle", 10);
|
||||
Settings.hikariMaximumLifetime = config.getLong("data_storage_settings.hikari_pool_settings.maximum_lifetime", 1800000);
|
||||
Settings.hikariKeepAliveTime = config.getLong("data_storage_settings.hikari_pool_settings.keepalive_time", 0);
|
||||
Settings.hikariConnectionTimeOut = config.getLong("data_storage_settings.hikari_pool_settings.connection_timeout", 5000);
|
||||
|
||||
Settings.bounceBackSynchronisation = config.getBoolean("bounce_back_synchronization", true);
|
||||
|
||||
// Read cluster data
|
||||
Configuration section = config.getSection("clusters");
|
||||
final String settingDatabaseName = Settings.mySQLDatabase != null ? Settings.mySQLDatabase : "HuskSync";
|
||||
for (String clusterId : section.getKeys()) {
|
||||
final String playerTableName = config.getString("clusters." + clusterId + ".player_table", "husksync_players");
|
||||
final String dataTableName = config.getString("clusters." + clusterId + ".data_table", "husksync_data");
|
||||
final String databaseName = config.getString("clusters." + clusterId + ".database", settingDatabaseName);
|
||||
Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessageStrings(Configuration config) {
|
||||
final HashMap<String,String> messages = new HashMap<>();
|
||||
for (String messageId : config.getKeys()) {
|
||||
messages.put(messageId, config.getString(messageId));
|
||||
}
|
||||
MessageManager.setMessages(messages);
|
||||
}
|
||||
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.md_5.bungee.config.Configuration;
|
||||
import net.md_5.bungee.config.ConfigurationProvider;
|
||||
import net.md_5.bungee.config.YamlConfiguration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public static void loadConfig() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getBungeeLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
if (!configFile.exists()) {
|
||||
Files.copy(plugin.getResourceAsStream("proxy-config.yml"), configFile.toPath());
|
||||
plugin.getBungeeLogger().info("Created HuskSync config file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveConfig(Configuration config) {
|
||||
try {
|
||||
ConfigurationProvider.getProvider(YamlConfiguration.class).save(config, new File(plugin.getDataFolder(), "config.yml"));
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessages() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getBungeeLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File messagesFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
if (!messagesFile.exists()) {
|
||||
Files.copy(plugin.getResourceAsStream("languages/" + Settings.language + ".yml"), messagesFile.toPath());
|
||||
plugin.getBungeeLogger().info("Created HuskSync messages file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An exception occurred loading the messages file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static Configuration getConfig() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the configuration file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Configuration getMessages() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
return ConfigurationProvider.getProvider(YamlConfiguration.class).load(configFile);
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.CONFIG, "An IOException occurred fetching the messages file", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.listener;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
import net.md_5.bungee.api.event.PostLoginEvent;
|
||||
import net.md_5.bungee.api.plugin.Listener;
|
||||
import net.md_5.bungee.event.EventHandler;
|
||||
import net.md_5.bungee.event.EventPriority;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeEventListener implements Listener {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
@EventHandler(priority = EventPriority.LOWEST)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
final ProxiedPlayer player = event.getPlayer();
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Ensure the player has data on SQL and that it is up-to-date
|
||||
HuskSyncBungeeCord.dataManager.ensurePlayerExists(player.getUniqueId(), player.getName());
|
||||
|
||||
// Get the player's data from SQL
|
||||
final Map<Settings.SynchronisationCluster, PlayerData> data = HuskSyncBungeeCord.dataManager.getPlayerData(player.getUniqueId());
|
||||
|
||||
// Update the player's data from SQL onto the cache
|
||||
assert data != null;
|
||||
for (Settings.SynchronisationCluster cluster : data.keySet()) {
|
||||
HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
|
||||
}
|
||||
|
||||
// Send a message asking the bukkit to request data on join
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.listener;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.ProxyServer;
|
||||
import net.md_5.bungee.api.connection.ProxiedPlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
// Initialize the listener on the bungee
|
||||
public BungeeRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
|
||||
PlayerData data = null;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(clusterId)) {
|
||||
// Get the player data from the cache
|
||||
PlayerData cachedData = HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).getPlayer(uuid);
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
data = Objects.requireNonNull(HuskSyncBungeeCord.dataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
|
||||
HuskSyncBungeeCord.dataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data; // Return the data
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages destined for Bukkit servers
|
||||
if (message.getMessageTarget().targetServerType() != Settings.ServerType.PROXY) {
|
||||
return;
|
||||
}
|
||||
// Only process redis messages when ready
|
||||
if (!HuskSyncBungeeCord.readyForRedis) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Get the UUID of the requesting player
|
||||
final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
|
||||
try {
|
||||
// Send the reply, serializing the message data
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
|
||||
.send();
|
||||
|
||||
// Send an update to all bukkit servers removing the player from the requester cache
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(requestingPlayerUUID);
|
||||
if (player != null) {
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize data when replying to a data request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
case PLAYER_DATA_UPDATE -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the data in the cache and SQL
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
|
||||
HuskSyncBungeeCord.dataManager.updatePlayerData(playerData, cluster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reply with the player data if they are still online (switching server)
|
||||
if (Settings.bounceBackSynchronisation && bounceBack) {
|
||||
try {
|
||||
ProxiedPlayer player = ProxyServer.getInstance().getPlayer(playerData.getPlayerUUID());
|
||||
if (player != null) {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
|
||||
serializedPlayerData)
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
player.sendMessage(ChatMessageType.ACTION_BAR, new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to re-serialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
case CONNECTION_HANDSHAKE -> {
|
||||
// Reply to a Bukkit server's connection handshake to complete the process
|
||||
if (HuskSyncBungeeCord.isDisabling) return; // Return if the Proxy is disabling
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final boolean hasMySqlPlayerDataBridge = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[2];
|
||||
final String huskSyncVersion = message.getMessageDataElements()[3];
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
serverUUID.toString(), plugin.getProxy().getName())
|
||||
.send();
|
||||
HuskSyncBungeeCord.synchronisedServers.add(
|
||||
new Server(serverUUID, hasMySqlPlayerDataBridge,
|
||||
huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
|
||||
log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize handshake message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case TERMINATE_HANDSHAKE -> {
|
||||
// Terminate the handshake with a Bukkit server
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[1];
|
||||
|
||||
// Remove a server from the synchronised server list
|
||||
Server serverToRemove = null;
|
||||
for (Server server : HuskSyncBungeeCord.synchronisedServers) {
|
||||
if (server.serverUUID().equals(serverUUID)) {
|
||||
serverToRemove = server;
|
||||
break;
|
||||
}
|
||||
}
|
||||
HuskSyncBungeeCord.synchronisedServers.remove(serverToRemove);
|
||||
log(Level.INFO, "Terminated the handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
}
|
||||
case DECODED_MPDB_DATA_SET -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final String playerName = message.getMessageDataElements()[1];
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling incoming decoded MPDB data");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the migrator
|
||||
MPDBMigrator migrator = HuskSyncBungeeCord.mpdbMigrator;
|
||||
|
||||
// Add the incoming data to the data to be saved
|
||||
migrator.incomingPlayerData.put(playerData, playerName);
|
||||
|
||||
// Increment players migrated
|
||||
migrator.playersMigrated++;
|
||||
plugin.getBungeeLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players.");
|
||||
|
||||
// When all the data has been received, save it
|
||||
if (migrator.migratedDataSent == migrator.playersMigrated) {
|
||||
ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> migrator.loadIncomingData(migrator.incomingPlayerData,
|
||||
HuskSyncBungeeCord.dataManager));
|
||||
}
|
||||
}
|
||||
case API_DATA_REQUEST -> ProxyServer.getInstance().getScheduler().runAsync(plugin, () -> {
|
||||
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
|
||||
|
||||
if (data == null) {
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString())
|
||||
.send();
|
||||
} else {
|
||||
// Send the reply alongside the request UUID, serializing the requested message data
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString(),
|
||||
RedisMessage.serialize(data))
|
||||
.send();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getBungeeLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getBungeeLogger().log(level, message);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.util;
|
||||
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public record BungeeLogger(java.util.logging.Logger parent) implements Logger {
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message, Exception e) {
|
||||
parent.log(level, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
parent.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
parent.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(String message) {
|
||||
parent.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(String message) {
|
||||
parent.config(message);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.bungeecord.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncBungeeCord;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BungeeUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncBungeeCord plugin = HuskSyncBungeeCord.getInstance();
|
||||
|
||||
public BungeeUpdateChecker(String versionToCheck) {
|
||||
super(versionToCheck);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getBungeeLogger().log(level, message);
|
||||
}
|
||||
}
|
@ -1,11 +1,23 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'dev.dejvokep:boosted-yaml:1.2'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly 'org.xerial:sqlite-jdbc:' + sqlite_driver_version
|
||||
compileOnly 'redis.clients:jedis:' + jedis_version
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.huskhomes.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries'
|
||||
relocate 'de.themoep', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.intellij', 'net.william278.huskhomes.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j'
|
||||
relocate 'com.google', 'net.william278.huskhomes.libraries'
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HuskSync {
|
||||
|
||||
@NotNull Set<OnlineUser> getOnlineUsers();
|
||||
|
||||
@NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid);
|
||||
|
||||
@NotNull EventListener getEventListener();
|
||||
|
||||
@NotNull Database getDatabase();
|
||||
|
||||
@NotNull RedisManager getRedisManager();
|
||||
|
||||
@NotNull Settings getSettings();
|
||||
|
||||
@NotNull Locales getLocales();
|
||||
|
||||
@NotNull Logger getLogger();
|
||||
|
||||
@NotNull String getVersion();
|
||||
|
||||
void reload();
|
||||
|
||||
}
|
@ -1,533 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.io.*;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Cross-platform class used to represent a player's data. Data from this can be deserialized using the DataSerializer class on Bukkit platforms.
|
||||
*/
|
||||
public class PlayerData implements Serializable {
|
||||
|
||||
/**
|
||||
* The UUID of the player who this data belongs to
|
||||
*/
|
||||
private final UUID playerUUID;
|
||||
|
||||
/**
|
||||
* The unique version UUID of this data
|
||||
*/
|
||||
private final UUID dataVersionUUID;
|
||||
|
||||
/**
|
||||
* Epoch time identifying when the data was last updated or created
|
||||
*/
|
||||
private long timestamp;
|
||||
|
||||
/**
|
||||
* A special flag that will be {@code true} if the player is new to the network and should not have their data set when joining the Bukkit
|
||||
*/
|
||||
public boolean useDefaultData = false;
|
||||
|
||||
/*
|
||||
* Player data records
|
||||
*/
|
||||
private String serializedInventory;
|
||||
private String serializedEnderChest;
|
||||
private double health;
|
||||
private double maxHealth;
|
||||
private double healthScale;
|
||||
private int hunger;
|
||||
private float saturation;
|
||||
private float saturationExhaustion;
|
||||
private int selectedSlot;
|
||||
private String serializedEffectData;
|
||||
private int totalExperience;
|
||||
private int expLevel;
|
||||
private float expProgress;
|
||||
private String gameMode;
|
||||
private String serializedStatistics;
|
||||
private boolean isFlying;
|
||||
private String serializedAdvancements;
|
||||
private String serializedLocation;
|
||||
|
||||
/**
|
||||
* Constructor to create new PlayerData from a bukkit {@code Player}'s data
|
||||
*
|
||||
* @param playerUUID The Player's UUID
|
||||
* @param serializedInventory Their serialized inventory
|
||||
* @param serializedEnderChest Their serialized ender chest
|
||||
* @param health Their health
|
||||
* @param maxHealth Their max health
|
||||
* @param healthScale Their health scale
|
||||
* @param hunger Their hunger
|
||||
* @param saturation Their saturation
|
||||
* @param saturationExhaustion Their saturation exhaustion
|
||||
* @param selectedSlot Their selected hot bar slot
|
||||
* @param serializedStatusEffects Their serialized status effects
|
||||
* @param totalExperience Their total experience points ("Score")
|
||||
* @param expLevel Their exp level
|
||||
* @param expProgress Their exp progress to the next level
|
||||
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, String serializedInventory, String serializedEnderChest, double health, double maxHealth,
|
||||
double healthScale, int hunger, float saturation, float saturationExhaustion, int selectedSlot,
|
||||
String serializedStatusEffects, int totalExperience, int expLevel, float expProgress, String gameMode,
|
||||
String serializedStatistics, boolean isFlying, String serializedAdvancements, String serializedLocation) {
|
||||
this.dataVersionUUID = UUID.randomUUID();
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
this.playerUUID = playerUUID;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.isFlying = isFlying;
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.serializedLocation = serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for a PlayerData object from an existing object that was stored in SQL
|
||||
*
|
||||
* @param playerUUID The player whose data this is' UUID
|
||||
* @param dataVersionUUID The PlayerData version UUID
|
||||
* @param serializedInventory Their serialized inventory
|
||||
* @param serializedEnderChest Their serialized ender chest
|
||||
* @param health Their health
|
||||
* @param maxHealth Their max health
|
||||
* @param healthScale Their health scale
|
||||
* @param hunger Their hunger
|
||||
* @param saturation Their saturation
|
||||
* @param saturationExhaustion Their saturation exhaustion
|
||||
* @param selectedSlot Their selected hot bar slot
|
||||
* @param serializedStatusEffects Their serialized status effects
|
||||
* @param totalExperience Their total experience points ("Score")
|
||||
* @param expLevel Their exp level
|
||||
* @param expProgress Their exp progress to the next level
|
||||
* @param gameMode Their game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
* @param serializedStatistics Their serialized statistics data (Displayed in Statistics menu in ESC menu)
|
||||
*/
|
||||
public PlayerData(UUID playerUUID, UUID dataVersionUUID, long timestamp, String serializedInventory, String serializedEnderChest,
|
||||
double health, double maxHealth, double healthScale, int hunger, float saturation, float saturationExhaustion,
|
||||
int selectedSlot, String serializedStatusEffects, int totalExperience, int expLevel, float expProgress,
|
||||
String gameMode, String serializedStatistics, boolean isFlying, String serializedAdvancements,
|
||||
String serializedLocation) {
|
||||
this.playerUUID = playerUUID;
|
||||
this.dataVersionUUID = dataVersionUUID;
|
||||
this.timestamp = timestamp;
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.serializedEffectData = serializedStatusEffects;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.isFlying = isFlying;
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.serializedLocation = serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default PlayerData for a new user
|
||||
*
|
||||
* @param playerUUID The bukkit Player's UUID
|
||||
* @return Default {@link PlayerData}
|
||||
*/
|
||||
public static PlayerData DEFAULT_PLAYER_DATA(UUID playerUUID) {
|
||||
PlayerData data = new PlayerData(playerUUID, "", "", 20,
|
||||
20, 20, 20, 10, 1, 0,
|
||||
"", 0, 0, 0, "SURVIVAL",
|
||||
"", false, "", "");
|
||||
data.useDefaultData = true;
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link UUID} of the player whose data this is
|
||||
*
|
||||
* @return the player's {@link UUID}
|
||||
*/
|
||||
public UUID getPlayerUUID() {
|
||||
return playerUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the unique version {@link UUID} of the PlayerData
|
||||
*
|
||||
* @return The unique data version
|
||||
*/
|
||||
public UUID getDataVersionUUID() {
|
||||
return dataVersionUUID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp when this data was created or last updated
|
||||
*
|
||||
* @return time since epoch of last data update or creation
|
||||
*/
|
||||
public long getDataTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized player {@code ItemStack[]} inventory
|
||||
*
|
||||
* @return The player's serialized inventory
|
||||
*/
|
||||
public String getSerializedInventory() {
|
||||
return serializedInventory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized player {@code ItemStack[]} ender chest
|
||||
*
|
||||
* @return The player's serialized ender chest
|
||||
*/
|
||||
public String getSerializedEnderChest() {
|
||||
return serializedEnderChest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's health value
|
||||
*
|
||||
* @return the player's health
|
||||
*/
|
||||
public double getHealth() {
|
||||
return health;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's max health value
|
||||
*
|
||||
* @return the player's max health
|
||||
*/
|
||||
public double getMaxHealth() {
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's health scale value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/Player.html#getHealthScale()}
|
||||
*
|
||||
* @return the player's health scaling value
|
||||
*/
|
||||
public double getHealthScale() {
|
||||
return healthScale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's hunger points
|
||||
*
|
||||
* @return the player's hunger level
|
||||
*/
|
||||
public int getHunger() {
|
||||
return hunger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's saturation points
|
||||
*
|
||||
* @return the player's saturation level
|
||||
*/
|
||||
public float getSaturation() {
|
||||
return saturation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's saturation exhaustion value {@see https://hub.spigotmc.org/javadocs/bukkit/org/bukkit/entity/HumanEntity.html#getExhaustion()}
|
||||
*
|
||||
* @return the player's saturation exhaustion
|
||||
*/
|
||||
public float getSaturationExhaustion() {
|
||||
return saturationExhaustion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of the player's currently selected hotbar slot
|
||||
*
|
||||
* @return the player's selected hotbar slot
|
||||
*/
|
||||
public int getSelectedSlot() {
|
||||
return selectedSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's current status effects
|
||||
*
|
||||
* @return the player's serialized status effect data
|
||||
*/
|
||||
public String getSerializedEffectData() {
|
||||
return serializedEffectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's total experience score (used for presenting the death screen score value)
|
||||
*
|
||||
* @return the player's total experience score
|
||||
*/
|
||||
public int getTotalExperience() {
|
||||
return totalExperience;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's statistics
|
||||
*
|
||||
* @return the player's serialized statistic records
|
||||
*/
|
||||
public String getSerializedStatistics() {
|
||||
return serializedStatistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's current experience level
|
||||
*
|
||||
* @return the player's exp level
|
||||
*/
|
||||
public int getExpLevel() {
|
||||
return expLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's progress to the next experience level
|
||||
*
|
||||
* @return the player's exp progress
|
||||
*/
|
||||
public float getExpProgress() {
|
||||
return expProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the player's current game mode as a string ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
*
|
||||
* @return the player's game mode
|
||||
*/
|
||||
public String getGameMode() {
|
||||
return gameMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the player is currently flying
|
||||
*
|
||||
* @return {@code true} if the player is in flight; {@code false} otherwise
|
||||
*/
|
||||
public boolean isFlying() {
|
||||
return isFlying;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's advancements
|
||||
*
|
||||
* @return the player's serialized advancement data
|
||||
*/
|
||||
public String getSerializedAdvancements() {
|
||||
return serializedAdvancements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized {@link String} of the player's current location
|
||||
*
|
||||
* @return the player's serialized location
|
||||
*/
|
||||
public String getSerializedLocation() {
|
||||
return serializedLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's inventory data
|
||||
*
|
||||
* @param serializedInventory A serialized {@code String}; new inventory data
|
||||
*/
|
||||
public void setSerializedInventory(String serializedInventory) {
|
||||
this.serializedInventory = serializedInventory;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's ender chest data
|
||||
*
|
||||
* @param serializedEnderChest A serialized {@code String}; new ender chest inventory data
|
||||
*/
|
||||
public void setSerializedEnderChest(String serializedEnderChest) {
|
||||
this.serializedEnderChest = serializedEnderChest;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's health
|
||||
*
|
||||
* @param health new health value
|
||||
*/
|
||||
public void setHealth(double health) {
|
||||
this.health = health;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's max health
|
||||
*
|
||||
* @param maxHealth new maximum health value
|
||||
*/
|
||||
public void setMaxHealth(double maxHealth) {
|
||||
this.maxHealth = maxHealth;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's health scale
|
||||
*
|
||||
* @param healthScale new health scaling value
|
||||
*/
|
||||
public void setHealthScale(double healthScale) {
|
||||
this.healthScale = healthScale;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's hunger meter
|
||||
*
|
||||
* @param hunger new hunger value
|
||||
*/
|
||||
public void setHunger(int hunger) {
|
||||
this.hunger = hunger;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's saturation level
|
||||
*
|
||||
* @param saturation new saturation value
|
||||
*/
|
||||
public void setSaturation(float saturation) {
|
||||
this.saturation = saturation;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's saturation exhaustion value
|
||||
*
|
||||
* @param saturationExhaustion new exhaustion value
|
||||
*/
|
||||
public void setSaturationExhaustion(float saturationExhaustion) {
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's selected hotbar slot
|
||||
*
|
||||
* @param selectedSlot new hotbar slot number (0-9)
|
||||
*/
|
||||
public void setSelectedSlot(int selectedSlot) {
|
||||
this.selectedSlot = selectedSlot;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's status effect data
|
||||
*
|
||||
* @param serializedEffectData A serialized {@code String} of the player's new status effect data
|
||||
*/
|
||||
public void setSerializedEffectData(String serializedEffectData) {
|
||||
this.serializedEffectData = serializedEffectData;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's total experience points (used to display score on death screen)
|
||||
*
|
||||
* @param totalExperience the player's new total experience score
|
||||
*/
|
||||
public void setTotalExperience(int totalExperience) {
|
||||
this.totalExperience = totalExperience;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's exp level
|
||||
*
|
||||
* @param expLevel the player's new exp level
|
||||
*/
|
||||
public void setExpLevel(int expLevel) {
|
||||
this.expLevel = expLevel;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's progress to their next exp level
|
||||
*
|
||||
* @param expProgress the player's new experience progress
|
||||
*/
|
||||
public void setExpProgress(float expProgress) {
|
||||
this.expProgress = expProgress;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the player's game mode
|
||||
*
|
||||
* @param gameMode the player's new game mode ({@code SURVIVAL}, {@code CREATIVE}, etc.)
|
||||
*/
|
||||
public void setGameMode(String gameMode) {
|
||||
this.gameMode = gameMode;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's statistics data
|
||||
*
|
||||
* @param serializedStatistics A serialized {@code String}; new statistic data
|
||||
*/
|
||||
public void setSerializedStatistics(String serializedStatistics) {
|
||||
this.serializedStatistics = serializedStatistics;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if the player is flying
|
||||
*
|
||||
* @param flying whether the player is flying
|
||||
*/
|
||||
public void setFlying(boolean flying) {
|
||||
isFlying = flying;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's advancement data
|
||||
*
|
||||
* @param serializedAdvancements A serialized {@code String}; new advancement data
|
||||
*/
|
||||
public void setSerializedAdvancements(String serializedAdvancements) {
|
||||
this.serializedAdvancements = serializedAdvancements;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the player's location data
|
||||
*
|
||||
* @param serializedLocation A serialized {@code String}; new location data
|
||||
*/
|
||||
public void setSerializedLocation(String serializedLocation) {
|
||||
this.serializedLocation = serializedLocation;
|
||||
this.timestamp = Instant.now().getEpochSecond();
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A record representing a server synchronised on the network and whether it has MySqlPlayerDataBridge installed
|
||||
*/
|
||||
public record Server(UUID serverUUID, boolean hasMySqlPlayerDataBridge, String huskSyncVersion, String serverBrand,
|
||||
String clusterId) {
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Settings class, holds values loaded from the plugin config (either Bukkit or Bungee)
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
/*
|
||||
* General settings
|
||||
*/
|
||||
|
||||
// Whether to do automatic update checks on startup
|
||||
public static boolean automaticUpdateChecks;
|
||||
|
||||
// The type of THIS server (Bungee or Bukkit)
|
||||
public static ServerType serverType;
|
||||
|
||||
// Redis settings
|
||||
public static String redisHost;
|
||||
public static int redisPort;
|
||||
public static String redisPassword;
|
||||
public static boolean redisSSL;
|
||||
|
||||
/*
|
||||
* Bungee / Proxy server-only settings
|
||||
*/
|
||||
|
||||
// Messages language
|
||||
public static String language;
|
||||
|
||||
// Cluster IDs
|
||||
public static ArrayList<SynchronisationCluster> clusters = new ArrayList<>();
|
||||
|
||||
// SQL settings
|
||||
public static DataStorageType dataStorageType;
|
||||
|
||||
// Bounce-back synchronisation (default)
|
||||
public static boolean bounceBackSynchronisation;
|
||||
|
||||
// MySQL specific settings
|
||||
public static String mySQLHost;
|
||||
public static String mySQLDatabase;
|
||||
public static String mySQLUsername;
|
||||
public static String mySQLPassword;
|
||||
public static int mySQLPort;
|
||||
public static String mySQLParams;
|
||||
|
||||
// Hikari connection pooling settings
|
||||
public static int hikariMaximumPoolSize;
|
||||
public static int hikariMinimumIdle;
|
||||
public static long hikariMaximumLifetime;
|
||||
public static long hikariKeepAliveTime;
|
||||
public static long hikariConnectionTimeOut;
|
||||
|
||||
/*
|
||||
* Bukkit server-only settings
|
||||
*/
|
||||
|
||||
// Synchronisation options
|
||||
public static boolean syncInventories;
|
||||
public static boolean syncEnderChests;
|
||||
public static boolean syncHealth;
|
||||
public static boolean syncHunger;
|
||||
public static boolean syncExperience;
|
||||
public static boolean syncPotionEffects;
|
||||
public static boolean syncStatistics;
|
||||
public static boolean syncGameMode;
|
||||
public static boolean syncAdvancements;
|
||||
public static boolean syncLocation;
|
||||
public static boolean syncFlight;
|
||||
public static long synchronizationTimeoutRetryDelay;
|
||||
public static boolean saveOnWorldSave;
|
||||
public static boolean useNativeImplementation;
|
||||
|
||||
// This Cluster ID
|
||||
public static String cluster;
|
||||
|
||||
/*
|
||||
* Enum definitions
|
||||
*/
|
||||
|
||||
public enum ServerType {
|
||||
BUKKIT,
|
||||
PROXY,
|
||||
}
|
||||
|
||||
public enum DataStorageType {
|
||||
MYSQL,
|
||||
SQLITE
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines information for a synchronisation cluster as listed on the proxy
|
||||
*/
|
||||
public record SynchronisationCluster(String clusterId, String databaseName, String playerTableName, String dataTableName) {
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Represents an abstract cross-platform representation for a plugin command
|
||||
*/
|
||||
public abstract class CommandBase {
|
||||
|
||||
/**
|
||||
* The input string to match for this command
|
||||
*/
|
||||
public final String command;
|
||||
|
||||
/**
|
||||
* The permission node required to use this command
|
||||
*/
|
||||
public final String permission;
|
||||
|
||||
/**
|
||||
* Alias input strings for this command
|
||||
*/
|
||||
public final String[] aliases;
|
||||
|
||||
/**
|
||||
* Instance of the implementing plugin
|
||||
*/
|
||||
public final HuskSync plugin;
|
||||
|
||||
|
||||
public CommandBase(@NotNull String command, @NotNull Permission permission, @NotNull HuskSync implementor, String... aliases) {
|
||||
this.command = command;
|
||||
this.permission = permission.node;
|
||||
this.plugin = implementor;
|
||||
this.aliases = aliases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires when the command is executed
|
||||
*
|
||||
* @param player {@link OnlineUser} executing the command
|
||||
* @param args Command arguments
|
||||
*/
|
||||
public abstract void onExecute(@NotNull OnlineUser player, @NotNull String[] args);
|
||||
|
||||
/**
|
||||
* Returns the localised description string of this command
|
||||
*
|
||||
* @return the command description
|
||||
*/
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(command + "_command_description")
|
||||
.orElse("A HuskHomes command");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Interface providing console execution of commands
|
||||
*/
|
||||
public interface ConsoleExecutable {
|
||||
|
||||
/**
|
||||
* What to do when console executes a command
|
||||
*
|
||||
* @param args command argument strings
|
||||
*/
|
||||
void onConsoleExecute(@NotNull String[] args);
|
||||
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync implementor) {
|
||||
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
displayPluginInformation(player);
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_UPDATE.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), plugin.getLogger());
|
||||
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
|
||||
if (updateChecker.isUpdateAvailable(latestVersion)) {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" +
|
||||
"[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
|
||||
"[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
|
||||
} else {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + latestVersion));
|
||||
}
|
||||
});
|
||||
}
|
||||
case "info", "about" -> displayPluginInformation(player);
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.reload();
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) �fb9a&| Reloaded config & message files."));
|
||||
}
|
||||
default ->
|
||||
plugin.getLocales().getLocale("error_invalid_syntax", "/husksync <update|info|reload>").ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConsoleExecute(@NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLogger().log(Level.INFO, "Console usage: /husksync <update|info|reload|migrate>");
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" -> new UpdateChecker(plugin.getVersion(), plugin.getLogger()).logToConsole();
|
||||
case "info", "about" -> plugin.getLogger().log(Level.INFO, plugin.getLocales().stripMineDown(
|
||||
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
|
||||
case "reload" -> {
|
||||
plugin.reload();
|
||||
plugin.getLogger().log(Level.INFO, "Reloaded config & message files.");
|
||||
}
|
||||
case "migrate" -> {
|
||||
//todo - MPDB migrator
|
||||
}
|
||||
default ->
|
||||
plugin.getLogger().log(Level.INFO, "Invalid syntax. Console usage: /husksync <update|info|reload|migrate>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void displayPluginInformation(@NotNull OnlineUser player) {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Static plugin permission nodes required to execute commands
|
||||
*/
|
||||
public enum Permission {
|
||||
|
||||
/*
|
||||
* /husksync command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /husksync} command (subcommand permissions required)
|
||||
*/
|
||||
COMMAND_HUSKSYNC("husksync.command.husksync", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user view plugin info {@code /husksync info}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_INFO("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user reload the plugin {@code /husksync reload}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_RELOAD("husksync.command.husksync.reload", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user view the plugin version and check for updates {@code /husksync update}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_UPDATE("husksync.command.husksync.update", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user save a player's data {@code /husksync save (player)}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_SAVE("husksync.command.husksync.save", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user save all online player data {@code /husksync saveall}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_SAVE_ALL("husksync.command.husksync.saveall", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user view a player's backup data {@code /husksync backup (player)}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_BACKUPS("husksync.command.husksync.backups", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user restore a player's backup data {@code /husksync backup (player) restore (backup_uuid)}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_BACKUPS_RESTORE("husksync.command.husksync.backups.restore", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /invsee command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /invsee (player)} command and view offline players' inventories
|
||||
*/
|
||||
COMMAND_VIEW_INVENTORIES("husksync.command.invsee", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' inventories
|
||||
*/
|
||||
COMMAND_EDIT_INVENTORIES("husksync.command.invsee.edit", DefaultAccess.OPERATORS),
|
||||
|
||||
/*
|
||||
* /echest command permissions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lets the user use the {@code /echest (player)} command and view offline players' ender chests
|
||||
*/
|
||||
COMMAND_VIEW_ENDER_CHESTS("husksync.command.echest", DefaultAccess.OPERATORS),
|
||||
/**
|
||||
* Lets the user edit the contents of offline players' ender chests
|
||||
*/
|
||||
COMMAND_EDIT_ENDER_CHESTS("husksync.command.echest.edit", DefaultAccess.OPERATORS);
|
||||
|
||||
public final String node;
|
||||
public final DefaultAccess defaultAccess;
|
||||
|
||||
Permission(@NotNull String node, @NotNull DefaultAccess defaultAccess) {
|
||||
this.node = node;
|
||||
this.defaultAccess = defaultAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies who gets what permissions by default
|
||||
*/
|
||||
public enum DefaultAccess {
|
||||
/**
|
||||
* Everyone gets this permission node by default
|
||||
*/
|
||||
EVERYONE,
|
||||
/**
|
||||
* Nobody gets this permission node by default
|
||||
*/
|
||||
NOBODY,
|
||||
/**
|
||||
* Server operators ({@code /op}) get this permission node by default
|
||||
*/
|
||||
OPERATORS
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface providing tab completions for a command
|
||||
*/
|
||||
public interface TabCompletable {
|
||||
|
||||
/**
|
||||
* What should be returned when the player attempts to TAB-complete the command
|
||||
*
|
||||
* @param player {@link OnlineUser} doing the TAB completion
|
||||
* @param args Current command arguments
|
||||
* @return List of String arguments to offer TAB suggestions
|
||||
*/
|
||||
List<String> onTabComplete(@NotNull OnlineUser player, @NotNull String[] args);
|
||||
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Loaded locales used by the plugin to display various locales
|
||||
*/
|
||||
public class Locales {
|
||||
|
||||
public static final String PLUGIN_INFORMATION = """
|
||||
[HuskSync](#00fb9a bold) [| Version %version%(#00fb9a)
|
||||
[A modern, cross-server player data synchronization system](gray)
|
||||
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
|
||||
[• Contributors:](white) [HarvelsX](gray show_text=&7Code)
|
||||
[• Translators:](white) [Namiu/うにたろう](gray show_text=&7Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua)
|
||||
[• Plugin Info:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/)
|
||||
[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
|
||||
[• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
|
||||
|
||||
@NotNull
|
||||
private final HashMap<String, String> rawLocales;
|
||||
|
||||
private Locales(@NotNull YamlDocument localesConfig) {
|
||||
this.rawLocales = new HashMap<>();
|
||||
for (String localeId : localesConfig.getRoutesAsStrings(false)) {
|
||||
rawLocales.put(localeId, localesConfig.getString(localeId));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an un-formatted locale loaded from the locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId) {
|
||||
if (rawLocales.containsKey(localeId)) {
|
||||
return Optional.of(rawLocales.get(localeId));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an un-formatted locale loaded from the locales file, with replacements applied
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId).map(locale -> applyReplacements(locale, replacements));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId) {
|
||||
return getRawLocale(localeId).map(MineDown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MineDown-formatted locale from the locales file, with replacements applied
|
||||
*
|
||||
* @param localeId String identifier of the locale, corresponding to a key in the file
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId, replacements).map(MineDown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply placeholder replacements to a raw locale
|
||||
*
|
||||
* @param rawLocale The raw, unparsed locale
|
||||
* @param replacements Ordered array of replacement strings to fill in placeholders with
|
||||
* @return the raw locale, with inserted placeholders
|
||||
*/
|
||||
private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
|
||||
int replacementIndexer = 1;
|
||||
for (String replacement : replacements) {
|
||||
String replacementString = "%" + replacementIndexer + "%";
|
||||
rawLocale = rawLocale.replace(replacementString, replacement);
|
||||
replacementIndexer = replacementIndexer + 1;
|
||||
}
|
||||
return rawLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the locales from a BoostedYaml {@link YamlDocument} locales file
|
||||
*
|
||||
* @param localesConfig The loaded {@link YamlDocument} locales.yml file
|
||||
* @return the loaded {@link Locales}
|
||||
*/
|
||||
public static Locales load(@NotNull YamlDocument localesConfig) {
|
||||
return new Locales(localesConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a string of basic MineDown formatting, used for displaying plugin info to console
|
||||
*
|
||||
* @param string The string to strip
|
||||
* @return The MineDown-stripped string
|
||||
*/
|
||||
public String stripMineDown(@NotNull String string) {
|
||||
final String[] in = string.split("\n");
|
||||
final StringBuilder out = new StringBuilder();
|
||||
String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)";
|
||||
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher m = pattern.matcher(in[i]);
|
||||
|
||||
if (m.find()) {
|
||||
out.append(in[i].replace(m.group(0), ""));
|
||||
out.append(m.group(2));
|
||||
} else {
|
||||
out.append(in[i]);
|
||||
}
|
||||
|
||||
if (i + 1 != in.length) {
|
||||
out.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,270 @@
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Settings used for the plugin, as read from the config file
|
||||
*/
|
||||
public class Settings {
|
||||
|
||||
/**
|
||||
* Map of {@link ConfigOption}s read from the config file
|
||||
*/
|
||||
private final HashMap<ConfigOption, Object> configOptions;
|
||||
|
||||
// Load the settings from the document
|
||||
private Settings(@NotNull YamlDocument config) {
|
||||
this.configOptions = new HashMap<>();
|
||||
Arrays.stream(ConfigOption.values()).forEach(configOption -> configOptions
|
||||
.put(configOption, switch (configOption.optionType) {
|
||||
case BOOLEAN -> configOption.getBooleanValue(config);
|
||||
case STRING -> configOption.getStringValue(config);
|
||||
case DOUBLE -> configOption.getDoubleValue(config);
|
||||
case FLOAT -> configOption.getFloatValue(config);
|
||||
case INTEGER -> configOption.getIntValue(config);
|
||||
case STRING_LIST -> configOption.getStringListValue(config);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a boolean
|
||||
* @throws ClassCastException if the option is not a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Boolean) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string
|
||||
* @throws ClassCastException if the option is not a string
|
||||
*/
|
||||
public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (String) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a double
|
||||
* @throws ClassCastException if the option is not a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Double) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a float
|
||||
* @throws ClassCastException if the option is not a float
|
||||
*/
|
||||
public double getFloatValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Float) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as an integer
|
||||
* @throws ClassCastException if the option is not an integer
|
||||
*/
|
||||
public int getIntegerValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Integer) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string {@link List}
|
||||
* @throws ClassCastException if the option is not a string list
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getStringListValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (List<String>) configOptions.get(option);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the settings from a BoostedYaml {@link YamlDocument} config file
|
||||
*
|
||||
* @param config The loaded {@link YamlDocument} config.yml file
|
||||
* @return the loaded {@link Settings}
|
||||
*/
|
||||
public static Settings load(@NotNull YamlDocument config) {
|
||||
return new Settings(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an option stored by a path in config.yml
|
||||
*/
|
||||
public enum ConfigOption {
|
||||
LANGUAGE("language", OptionType.STRING, "en-gb"),
|
||||
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
|
||||
|
||||
CLUSTER_ID("cluster_id", OptionType.STRING, ""), //todo implement this
|
||||
|
||||
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
|
||||
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
|
||||
DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"),
|
||||
DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"),
|
||||
DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"),
|
||||
DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"),
|
||||
DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10),
|
||||
DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10),
|
||||
DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000),
|
||||
DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0),
|
||||
DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000),
|
||||
DATABASE_PLAYERS_TABLE_NAME("database.table_names.players_table", OptionType.STRING, "husksync_players"),
|
||||
DATABASE_DATA_TABLE_NAME("database.table_names.data_table", OptionType.STRING, "husksync_data"),
|
||||
|
||||
REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"),
|
||||
REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379),
|
||||
REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
|
||||
REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
|
||||
|
||||
SYNCHRONIZATION_MAX_USER_DATA_RECORDS("synchronization.max_user_data_records", OptionType.INTEGER, 5),
|
||||
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_EXPERIENCE("synchronization.features.experience", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_POTION_EFFECTS("synchronization.features.potion_effects", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_ADVANCEMENTS("synchronization.features.advancements", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true),
|
||||
SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true);
|
||||
|
||||
/**
|
||||
* The path in the config.yml file to the value
|
||||
*/
|
||||
@NotNull
|
||||
public final String configPath;
|
||||
|
||||
/**
|
||||
* The {@link OptionType} of this option
|
||||
*/
|
||||
@NotNull
|
||||
public final OptionType optionType;
|
||||
|
||||
/**
|
||||
* The default value of this option if not set in config
|
||||
*/
|
||||
@Nullable
|
||||
private final Object defaultValue;
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType, @Nullable Object defaultValue) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string
|
||||
*/
|
||||
public String getStringValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getString(configPath, (String) defaultValue)
|
||||
: config.getString(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a boolean
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getBoolean(configPath, (Boolean) defaultValue)
|
||||
: config.getBoolean(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a double
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getDouble(configPath, (Double) defaultValue)
|
||||
: config.getDouble(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a float
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a float
|
||||
*/
|
||||
public float getFloatValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getFloat(configPath, (Float) defaultValue)
|
||||
: config.getFloat(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as an int
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as an int
|
||||
*/
|
||||
public int getIntValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getInt(configPath, (Integer) defaultValue)
|
||||
: config.getInt(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string {@link List}
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string {@link List}
|
||||
*/
|
||||
public List<String> getStringListValue(@NotNull YamlDocument config) {
|
||||
return config.getStringList(configPath, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the type of the object
|
||||
*/
|
||||
public enum OptionType {
|
||||
BOOLEAN,
|
||||
STRING,
|
||||
DOUBLE,
|
||||
FLOAT,
|
||||
INTEGER,
|
||||
STRING_LIST
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A mapped piece of advancement data
|
||||
*/
|
||||
public class AdvancementData {
|
||||
|
||||
/**
|
||||
* The advancement namespaced key
|
||||
*/
|
||||
@SerializedName("key")
|
||||
public String key;
|
||||
|
||||
/**
|
||||
* A map of completed advancement criteria to when it was completed
|
||||
*/
|
||||
@SerializedName("completed_criteria")
|
||||
public Map<String, Date> completedCriteria;
|
||||
|
||||
public AdvancementData() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores information about a player's inventory or ender chest
|
||||
*/
|
||||
public class InventoryData {
|
||||
|
||||
/**
|
||||
* A base64 string of platform-serialized inventory data
|
||||
*/
|
||||
@SerializedName("serialized_inventory")
|
||||
public String serializedInventory;
|
||||
|
||||
public InventoryData(@NotNull final String serializedInventory) {
|
||||
this.serializedInventory = serializedInventory;
|
||||
}
|
||||
|
||||
public InventoryData() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores information about a player's location
|
||||
*/
|
||||
public class LocationData {
|
||||
|
||||
/**
|
||||
* Name of the world on the server
|
||||
*/
|
||||
@SerializedName("world_name")
|
||||
public String worldName;
|
||||
/**
|
||||
* Unique id of the world
|
||||
*/
|
||||
@SerializedName("world_uuid")
|
||||
public UUID worldUuid;
|
||||
/**
|
||||
* The environment type of the world (one of "NORMAL", "NETHER", "THE_END")
|
||||
*/
|
||||
@SerializedName("world_environment")
|
||||
public String worldEnvironment;
|
||||
|
||||
/**
|
||||
* The x coordinate of the location
|
||||
*/
|
||||
@SerializedName("x")
|
||||
public double x;
|
||||
/**
|
||||
* The y coordinate of the location
|
||||
*/
|
||||
@SerializedName("y")
|
||||
public double y;
|
||||
/**
|
||||
* The z coordinate of the location
|
||||
*/
|
||||
@SerializedName("z")
|
||||
public double z;
|
||||
|
||||
/**
|
||||
* The location's facing yaw angle
|
||||
*/
|
||||
@SerializedName("yaw")
|
||||
public float yaw;
|
||||
/**
|
||||
* The location's facing pitch angle
|
||||
*/
|
||||
@SerializedName("pitch")
|
||||
public float pitch;
|
||||
|
||||
public LocationData() {
|
||||
}
|
||||
|
||||
public LocationData(@NotNull String worldName, @NotNull UUID worldUuid,
|
||||
@NotNull String worldEnvironment,
|
||||
double x, double y, double z,
|
||||
float yaw, float pitch) {
|
||||
this.worldName = worldName;
|
||||
this.worldUuid = worldUuid;
|
||||
this.worldEnvironment = worldEnvironment;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.yaw = yaw;
|
||||
this.pitch = pitch;
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Store's a user's persistent data container, holding a map of plugin-set persistent values
|
||||
*/
|
||||
public class PersistentDataContainerData {
|
||||
|
||||
/**
|
||||
* A base64 string of platform-serialized PersistentDataContainer data
|
||||
*/
|
||||
@SerializedName("serialized_persistent_data_container")
|
||||
public String serializedPersistentDataContainer;
|
||||
|
||||
public PersistentDataContainerData(@NotNull final String serializedPersistentDataContainer) {
|
||||
this.serializedPersistentDataContainer = serializedPersistentDataContainer;
|
||||
}
|
||||
|
||||
public PersistentDataContainerData() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
/**
|
||||
* Stores potion effect data
|
||||
*/
|
||||
public class PotionEffectData {
|
||||
|
||||
@SerializedName("serialized_potion_effects")
|
||||
public String serializedPotionEffects;
|
||||
|
||||
public PotionEffectData(@NotNull final String serializedInventory) {
|
||||
this.serializedPotionEffects = serializedInventory;
|
||||
}
|
||||
|
||||
public PotionEffectData() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Stores information about a player's statistics
|
||||
*/
|
||||
public class StatisticsData {
|
||||
|
||||
/**
|
||||
* Map of untyped statistic names to their values
|
||||
*/
|
||||
@SerializedName("untyped_statistics")
|
||||
public HashMap<String, Integer> untypedStatistic;
|
||||
|
||||
/**
|
||||
* Map of block type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("block_statistics")
|
||||
public HashMap<String, HashMap<String, Integer>> blockStatistics;
|
||||
|
||||
/**
|
||||
* Map of item type statistics to a map of material types to values
|
||||
*/
|
||||
@SerializedName("item_statistics")
|
||||
public HashMap<String, HashMap<String, Integer>> itemStatistics;
|
||||
|
||||
/**
|
||||
* Map of entity type statistics to a map of entity types to values
|
||||
*/
|
||||
@SerializedName("entity_statistics")
|
||||
public HashMap<String, HashMap<String, Integer>> entityStatistics;
|
||||
|
||||
public StatisticsData(@NotNull HashMap<String, Integer> untypedStatistic,
|
||||
@NotNull HashMap<String, HashMap<String, Integer>> blockStatistics,
|
||||
@NotNull HashMap<String, HashMap<String, Integer>> itemStatistics,
|
||||
@NotNull HashMap<String, HashMap<String, Integer>> entityStatistics) {
|
||||
this.untypedStatistic = untypedStatistic;
|
||||
this.blockStatistics = blockStatistics;
|
||||
this.itemStatistics = itemStatistics;
|
||||
this.entityStatistics = entityStatistics;
|
||||
}
|
||||
|
||||
public StatisticsData() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Stores status information about a player
|
||||
*/
|
||||
public class StatusData {
|
||||
|
||||
/**
|
||||
* The player's health points
|
||||
*/
|
||||
@SerializedName("health")
|
||||
public double health;
|
||||
|
||||
/**
|
||||
* The player's maximum health points
|
||||
*/
|
||||
@SerializedName("max_health")
|
||||
public double maxHealth;
|
||||
|
||||
/**
|
||||
* The player's health scaling factor
|
||||
*/
|
||||
@SerializedName("health_scale")
|
||||
public double healthScale;
|
||||
|
||||
/**
|
||||
* The player's hunger points
|
||||
*/
|
||||
@SerializedName("hunger")
|
||||
public int hunger;
|
||||
|
||||
/**
|
||||
* The player's saturation points
|
||||
*/
|
||||
@SerializedName("saturation")
|
||||
public float saturation;
|
||||
|
||||
/**
|
||||
* The player's saturation exhaustion points
|
||||
*/
|
||||
@SerializedName("saturation_exhaustion")
|
||||
public float saturationExhaustion;
|
||||
|
||||
/**
|
||||
* The player's currently selected item slot
|
||||
*/
|
||||
@SerializedName("selected_item_slot")
|
||||
public int selectedItemSlot;
|
||||
|
||||
/**
|
||||
* The player's total experience points<p>
|
||||
* (not to be confused with <i>experience level</i> - this is the "points" value shown on the death screen)
|
||||
*/
|
||||
@SerializedName("total_experience")
|
||||
public int totalExperience;
|
||||
|
||||
/**
|
||||
* The player's experience level (shown on the exp bar)
|
||||
*/
|
||||
@SerializedName("experience_level")
|
||||
public int expLevel;
|
||||
|
||||
/**
|
||||
* The player's progress to their next experience level
|
||||
*/
|
||||
@SerializedName("experience_progress")
|
||||
public float expProgress;
|
||||
|
||||
/**
|
||||
* The player's game mode string (one of "survival", "creative", "adventure", "spectator")
|
||||
*/
|
||||
@SerializedName("game_mode")
|
||||
public String gameMode;
|
||||
|
||||
/**
|
||||
* If the player is currently flying
|
||||
*/
|
||||
@SerializedName("is_flying")
|
||||
public boolean isFlying;
|
||||
|
||||
public StatusData(final double health, final double maxHealth, final double healthScale,
|
||||
final int hunger, final float saturation, final float saturationExhaustion,
|
||||
final int selectedItemSlot, final int totalExperience, final int expLevel,
|
||||
final float expProgress, final String gameMode, final boolean isFlying) {
|
||||
this.health = health;
|
||||
this.maxHealth = maxHealth;
|
||||
this.healthScale = healthScale;
|
||||
this.hunger = hunger;
|
||||
this.saturation = saturation;
|
||||
this.saturationExhaustion = saturationExhaustion;
|
||||
this.selectedItemSlot = selectedItemSlot;
|
||||
this.totalExperience = totalExperience;
|
||||
this.expLevel = expLevel;
|
||||
this.expProgress = expProgress;
|
||||
this.gameMode = gameMode;
|
||||
this.isFlying = isFlying;
|
||||
}
|
||||
|
||||
public StatusData() {
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
|
||||
/***
|
||||
* Stores data about a user
|
||||
*/
|
||||
public class UserData implements Comparable<UserData> {
|
||||
|
||||
/**
|
||||
* The unique identifier for this user data version
|
||||
*/
|
||||
protected UUID dataUuidVersion;
|
||||
|
||||
/**
|
||||
* An epoch milliseconds timestamp of when this data was created
|
||||
*/
|
||||
protected long creationTimestamp;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
*/
|
||||
@SerializedName("status")
|
||||
protected StatusData statusData;
|
||||
|
||||
/**
|
||||
* Stores the user's inventory contents
|
||||
*/
|
||||
@SerializedName("inventory")
|
||||
protected InventoryData inventoryData;
|
||||
|
||||
/**
|
||||
* Stores the user's ender chest contents
|
||||
*/
|
||||
@SerializedName("ender_chest")
|
||||
protected InventoryData enderChestData;
|
||||
|
||||
/**
|
||||
* Store's the user's potion effects
|
||||
*/
|
||||
@SerializedName("potion_effects")
|
||||
protected PotionEffectData potionEffectData;
|
||||
|
||||
/**
|
||||
* Stores the set of this user's advancements
|
||||
*/
|
||||
@SerializedName("advancements")
|
||||
protected HashSet<AdvancementData> advancementData;
|
||||
|
||||
/**
|
||||
* Stores the user's set of statistics
|
||||
*/
|
||||
@SerializedName("statistics")
|
||||
protected StatisticsData statisticData;
|
||||
|
||||
/**
|
||||
* Store's the user's world location and coordinates
|
||||
*/
|
||||
@SerializedName("location")
|
||||
protected LocationData locationData;
|
||||
|
||||
/**
|
||||
* Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
|
||||
*/
|
||||
@SerializedName("persistent_data_container")
|
||||
protected PersistentDataContainerData persistentDataContainerData;
|
||||
|
||||
public UserData(@NotNull StatusData statusData, @NotNull InventoryData inventoryData,
|
||||
@NotNull InventoryData enderChestData, @NotNull PotionEffectData potionEffectData,
|
||||
@NotNull HashSet<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
|
||||
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) {
|
||||
this.dataUuidVersion = UUID.randomUUID();
|
||||
this.creationTimestamp = Instant.now().toEpochMilli();
|
||||
this.statusData = statusData;
|
||||
this.inventoryData = inventoryData;
|
||||
this.enderChestData = enderChestData;
|
||||
this.potionEffectData = potionEffectData;
|
||||
this.advancementData = advancementData;
|
||||
this.statisticData = statisticData;
|
||||
this.locationData = locationData;
|
||||
this.persistentDataContainerData = persistentDataContainerData;
|
||||
}
|
||||
|
||||
protected UserData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UserData by creation timestamp
|
||||
*
|
||||
* @param other the other UserData to be compared
|
||||
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(@NotNull UserData other) {
|
||||
return Long.compare(this.creationTimestamp, other.creationTimestamp);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static UserData fromJson(String json) throws JsonSyntaxException {
|
||||
return new GsonBuilder().create().fromJson(json, UserData.class);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String toJson() {
|
||||
return new GsonBuilder().create().toJson(this);
|
||||
}
|
||||
|
||||
public void setMetadata(@NotNull UUID dataUuidVersion, long creationTimestamp) {
|
||||
this.dataUuidVersion = dataUuidVersion;
|
||||
this.creationTimestamp = creationTimestamp;
|
||||
}
|
||||
|
||||
public UUID getDataUuidVersion() {
|
||||
return dataUuidVersion;
|
||||
}
|
||||
|
||||
public long getCreationTimestamp() {
|
||||
return creationTimestamp;
|
||||
}
|
||||
|
||||
public StatusData getStatusData() {
|
||||
return statusData;
|
||||
}
|
||||
|
||||
public InventoryData getInventoryData() {
|
||||
return inventoryData;
|
||||
}
|
||||
|
||||
public InventoryData getEnderChestData() {
|
||||
return enderChestData;
|
||||
}
|
||||
|
||||
public PotionEffectData getPotionEffectData() {
|
||||
return potionEffectData;
|
||||
}
|
||||
|
||||
public HashSet<AdvancementData> getAdvancementData() {
|
||||
return advancementData;
|
||||
}
|
||||
|
||||
public StatisticsData getStatisticData() {
|
||||
return statisticData;
|
||||
}
|
||||
|
||||
public LocationData getLocationData() {
|
||||
return locationData;
|
||||
}
|
||||
|
||||
public PersistentDataContainerData getPersistentDataContainerData() {
|
||||
return persistentDataContainerData;
|
||||
}
|
||||
}
|
@ -0,0 +1,156 @@
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* An abstract representation of the plugin database, storing player data.
|
||||
* <p>
|
||||
* Implemented by different database platforms - MySQL, SQLite, etc. - as configured by the administrator.
|
||||
*/
|
||||
public abstract class Database {
|
||||
|
||||
/**
|
||||
* Name of the table that stores player information
|
||||
*/
|
||||
protected final String playerTableName;
|
||||
|
||||
/**
|
||||
* Name of the table that stores data
|
||||
*/
|
||||
protected final String dataTableName;
|
||||
|
||||
/**
|
||||
* The maximum number of user records to store in the database at once per user
|
||||
*/
|
||||
protected final int maxUserDataRecords;
|
||||
|
||||
/**
|
||||
* Logger instance used for database error logging
|
||||
*/
|
||||
private final Logger logger;
|
||||
|
||||
/**
|
||||
* Returns the {@link Logger} used to log database errors
|
||||
*
|
||||
* @return the {@link Logger} instance
|
||||
*/
|
||||
protected Logger getLogger() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The {@link ResourceReader} used to read internal resource files by name
|
||||
*/
|
||||
private final ResourceReader resourceReader;
|
||||
|
||||
protected Database(@NotNull String playerTableName, @NotNull String dataTableName, final int maxUserDataRecords,
|
||||
@NotNull ResourceReader resourceReader, @NotNull Logger logger) {
|
||||
this.playerTableName = playerTableName;
|
||||
this.dataTableName = dataTableName;
|
||||
this.maxUserDataRecords = maxUserDataRecords;
|
||||
this.resourceReader = resourceReader;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads SQL table creation schema statements from a resource file as a string array
|
||||
*
|
||||
* @param schemaFileName database script resource file to load from
|
||||
* @return Array of string-formatted table creation schema statements
|
||||
* @throws IOException if the resource could not be read
|
||||
*/
|
||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||
return formatStatementTables(
|
||||
new String(resourceReader.getResource(schemaFileName)
|
||||
.readAllBytes(), StandardCharsets.UTF_8))
|
||||
.split(";");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format all table name placeholder strings in a SQL statement
|
||||
*
|
||||
* @param sql the SQL statement with un-formatted table name placeholders
|
||||
* @return the formatted statement, with table placeholders replaced with the correct names
|
||||
*/
|
||||
protected final String formatStatementTables(@NotNull String sql) {
|
||||
return sql.replaceAll("%players_table%", playerTableName)
|
||||
.replaceAll("%data_table%", dataTableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the database and ensure tables are present; create tables if they do not exist.
|
||||
*
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> initialize();
|
||||
|
||||
/**
|
||||
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
|
||||
*
|
||||
* @param user The {@link User} to ensure
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> ensureUser(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get a player by their Minecraft account {@link UUID}
|
||||
*
|
||||
* @param uuid Minecraft account {@link UUID} of the {@link User} to get
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid);
|
||||
|
||||
/**
|
||||
* Get a user by their username (<i>case-insensitive</i>)
|
||||
*
|
||||
* @param username Username of the {@link User} to get (<i>case-insensitive</i>)
|
||||
* @return A future returning an optional with the {@link User} present if they exist
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<User>> getUserByName(@NotNull String username);
|
||||
|
||||
/**
|
||||
* Get the current user data for a given user, if it exists.
|
||||
*
|
||||
* @param user the user to get data for
|
||||
* @return an optional containing the user data, if it exists, or an empty optional if it does not
|
||||
*/
|
||||
public abstract CompletableFuture<Optional<UserData>> getCurrentUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Get all UserData entries for a user from the database.
|
||||
*
|
||||
* @param user The user to get data for
|
||||
* @return A future returning a list of a user's data
|
||||
*/
|
||||
public abstract CompletableFuture<List<UserData>> getUserData(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Prune user data records for a given user to the maximum value as configured
|
||||
*
|
||||
* @param user The user to prune data for
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
protected abstract CompletableFuture<Void> pruneUserDataRecords(@NotNull User user);
|
||||
|
||||
/**
|
||||
* Add user data to the database<p>
|
||||
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
|
||||
*
|
||||
* @param user The user to add data for
|
||||
* @param userData The data to add
|
||||
* @return A future returning void when complete
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData);
|
||||
|
||||
}
|
@ -0,0 +1,289 @@
|
||||
package net.william278.husksync.database;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.*;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MySqlDatabase extends Database {
|
||||
/**
|
||||
* MySQL server hostname
|
||||
*/
|
||||
private final String mySqlHost;
|
||||
|
||||
/**
|
||||
* MySQL server port
|
||||
*/
|
||||
private final int mySqlPort;
|
||||
|
||||
/**
|
||||
* Database to use on the MySQL server
|
||||
*/
|
||||
private final String mySqlDatabaseName;
|
||||
private final String mySqlUsername;
|
||||
private final String mySqlPassword;
|
||||
private final String mySqlConnectionParameters;
|
||||
|
||||
private final int hikariMaximumPoolSize;
|
||||
private final int hikariMinimumIdle;
|
||||
private final int hikariMaximumLifetime;
|
||||
private final int hikariKeepAliveTime;
|
||||
private final int hikariConnectionTimeOut;
|
||||
|
||||
private static final String DATA_POOL_NAME = "HuskHomesHikariPool";
|
||||
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger) {
|
||||
super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME),
|
||||
settings.getStringValue(Settings.ConfigOption.DATABASE_DATA_TABLE_NAME),
|
||||
settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_RECORDS),
|
||||
resourceReader, logger);
|
||||
mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
|
||||
mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
|
||||
mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||
mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
|
||||
mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
|
||||
mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS);
|
||||
hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE);
|
||||
hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE);
|
||||
hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME);
|
||||
hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE);
|
||||
hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the auto-closeable connection from the hikariDataSource
|
||||
*
|
||||
* @return The {@link Connection} to the MySQL database
|
||||
* @throws SQLException if the connection fails for some reason
|
||||
*/
|
||||
private Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> initialize() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Create jdbc driver connection url
|
||||
final String jdbcUrl = "jdbc:mysql://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters;
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(jdbcUrl);
|
||||
|
||||
// Authenticate
|
||||
dataSource.setUsername(mySqlUsername);
|
||||
dataSource.setPassword(mySqlPassword);
|
||||
|
||||
// Set various additional parameters
|
||||
dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
|
||||
dataSource.setMinimumIdle(hikariMinimumIdle);
|
||||
dataSource.setMaxLifetime(hikariMaximumLifetime);
|
||||
dataSource.setKeepaliveTime(hikariKeepAliveTime);
|
||||
dataSource.setConnectionTimeout(hikariConnectionTimeOut);
|
||||
dataSource.setPoolName(DATA_POOL_NAME);
|
||||
|
||||
// Prepare database schema; make tables if they don't exist
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
// Load database schema CREATE statements from schema file
|
||||
final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql");
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : databaseSchema) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
}
|
||||
} catch (SQLException | IOException e) {
|
||||
getLogger().log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> ensureUser(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> getUser(user.uuid).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(existingUser -> {
|
||||
if (!existingUser.username.equals(user.username)) {
|
||||
// Update a user's name if it has changed in the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
UPDATE `%players_table%`
|
||||
SET `username`=?
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, user.username);
|
||||
statement.setString(2, existingUser.uuid.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
getLogger().log(Level.INFO, "Updated " + user.username + "'s name in the database (" + existingUser.username + " -> " + user.username + ")");
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to update a user's name on the database", e);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Insert new player data into the database
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%players_table%` (`uuid`,`username`)
|
||||
VALUES (?,?);"""))) {
|
||||
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, user.username);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUser(@NotNull UUID uuid) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%players_table%`
|
||||
WHERE `uuid`=?"""))) {
|
||||
|
||||
statement.setString(1, uuid.toString());
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user from uuid from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<User>> getUserByName(@NotNull String username) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `uuid`, `username`
|
||||
FROM `%players_table%`
|
||||
WHERE `username`=?"""))) {
|
||||
statement.setString(1, username);
|
||||
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
return Optional.of(new User(UUID.fromString(resultSet.getString("uuid")),
|
||||
resultSet.getString("username")));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user by name from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Optional<UserData>> getCurrentUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `data`
|
||||
FROM `%data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final UserData data = UserData.fromJson(resultSet.getString("data"));
|
||||
data.setMetadata(UUID.fromString(resultSet.getString("version_uuid")),
|
||||
resultSet.getTimestamp("timestamp").toInstant().toEpochMilli());
|
||||
return Optional.of(data);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final ArrayList<UserData> retrievedData = new ArrayList<>();
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
SELECT `version_uuid`, `timestamp`, `data`
|
||||
FROM `%data_table%`
|
||||
WHERE `player_uuid`=?
|
||||
ORDER BY `timestamp` DESC;"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
final ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UserData data = UserData.fromJson(resultSet.getString("data"));
|
||||
data.setMetadata(UUID.fromString(resultSet.getString("version_uuid")),
|
||||
resultSet.getTimestamp("timestamp").toInstant().toEpochMilli());
|
||||
retrievedData.add(data);
|
||||
}
|
||||
return retrievedData;
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
|
||||
}
|
||||
return retrievedData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Void> pruneUserDataRecords(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> {
|
||||
if (data.size() > maxUserDataRecords) {
|
||||
Collections.reverse(data);
|
||||
data.subList(0, data.size() - maxUserDataRecords).forEach(dataToDelete -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
DELETE FROM `%data_table%`
|
||||
WHERE `version_uuid`=?"""))) {
|
||||
statement.setString(1, dataToDelete.getDataUuidVersion().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Connection connection = getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||
INSERT INTO `%data_table%`
|
||||
(`player_uuid`,`version_uuid`,`timestamp`,`data`)
|
||||
VALUES (?,?,?,?);"""))) {
|
||||
statement.setString(1, user.uuid.toString());
|
||||
statement.setString(2, userData.getDataUuidVersion().toString());
|
||||
statement.setTimestamp(3, Timestamp.from(Instant.ofEpochMilli(userData.getCreationTimestamp())));
|
||||
statement.setString(4, userData.toJson());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||
}
|
||||
}).thenRunAsync(() -> pruneUserDataRecords(user).join());
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class EventListener {
|
||||
|
||||
private final HuskSync huskSync;
|
||||
private final HashSet<UUID> usersAwaitingSync;
|
||||
|
||||
protected EventListener(@NotNull HuskSync huskSync) {
|
||||
this.huskSync = huskSync;
|
||||
this.usersAwaitingSync = new HashSet<>();
|
||||
}
|
||||
|
||||
public final void handlePlayerJoin(@NotNull OnlineUser user) {
|
||||
usersAwaitingSync.add(user.uuid);
|
||||
huskSync.getRedisManager().getUserData(user, RedisManager.RedisKeyType.SERVER_CHANGE).thenAccept(
|
||||
cachedUserData -> cachedUserData.ifPresentOrElse(
|
||||
userData -> user.setData(userData, huskSync.getSettings()).join(),
|
||||
() -> huskSync.getDatabase().getCurrentUserData(user).thenAccept(
|
||||
databaseUserData -> databaseUserData.ifPresent(
|
||||
data -> user.setData(data, huskSync.getSettings()).join())).join())).thenRunAsync(
|
||||
() -> {
|
||||
huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
|
||||
usersAwaitingSync.remove(user.uuid);
|
||||
huskSync.getDatabase().ensureUser(user).join();
|
||||
});
|
||||
}
|
||||
|
||||
public final void handlePlayerQuit(@NotNull OnlineUser user) {
|
||||
user.getUserData().thenAccept(userData -> huskSync.getRedisManager()
|
||||
.setPlayerData(user, userData, RedisManager.RedisKeyType.SERVER_CHANGE).thenRun(
|
||||
() -> huskSync.getDatabase().setUserData(user, userData).join()));
|
||||
}
|
||||
|
||||
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {
|
||||
CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->
|
||||
huskSync.getDatabase().setUserData(user, user.getUserData().join()).join()));
|
||||
}
|
||||
|
||||
public final boolean cancelPlayerEvent(@NotNull OnlineUser user) {
|
||||
return usersAwaitingSync.contains(user.uuid);
|
||||
}
|
||||
|
||||
}
|
@ -1,312 +0,0 @@
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.proxy.data.DataManager;
|
||||
import net.william278.husksync.proxy.data.sql.Database;
|
||||
import net.william278.husksync.proxy.data.sql.MySQL;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Class to handle migration of data from MySQLPlayerDataBridge
|
||||
* <p>
|
||||
* The migrator accesses and decodes MPDB's format directly,
|
||||
* by communicating with a Spigot server
|
||||
*/
|
||||
public class MPDBMigrator {
|
||||
|
||||
public int migratedDataSent = 0;
|
||||
public int playersMigrated = 0;
|
||||
|
||||
public HashMap<PlayerData, String> incomingPlayerData;
|
||||
|
||||
public MigrationSettings migrationSettings = new MigrationSettings();
|
||||
private Settings.SynchronisationCluster targetCluster;
|
||||
private Database sourceDatabase;
|
||||
|
||||
private HashSet<MPDBPlayerData> mpdbPlayerData;
|
||||
|
||||
private final Logger logger;
|
||||
|
||||
public MPDBMigrator(Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public boolean readyToMigrate(int networkPlayerCount, HashSet<Server> synchronisedServers) {
|
||||
if (networkPlayerCount > 0) {
|
||||
logger.log(Level.WARNING, "Failed to start migration because there are players online. " +
|
||||
"Your network has to be empty to migrate data for safety reasons.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int synchronisedServersWithMpdb = 0;
|
||||
for (Server server : synchronisedServers) {
|
||||
if (server.hasMySqlPlayerDataBridge()) {
|
||||
synchronisedServersWithMpdb++;
|
||||
}
|
||||
}
|
||||
if (synchronisedServersWithMpdb < 1) {
|
||||
logger.log(Level.WARNING, "Failed to start migration because at least one Spigot server with both HuskSync and MySqlPlayerDataBridge installed is not online. " +
|
||||
"Please start one Spigot server with HuskSync installed to begin migration.");
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (migrationSettings.targetCluster.equals(cluster.clusterId())) {
|
||||
targetCluster = cluster;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetCluster == null) {
|
||||
logger.log(Level.WARNING, "Failed to start migration because the target cluster could not be found. " +
|
||||
"Please ensure the target cluster is correct, configured in the proxy config file, then try again");
|
||||
return false;
|
||||
}
|
||||
|
||||
migratedDataSent = 0;
|
||||
playersMigrated = 0;
|
||||
mpdbPlayerData = new HashSet<>();
|
||||
incomingPlayerData = new HashMap<>();
|
||||
final MigrationSettings settings = migrationSettings;
|
||||
|
||||
// Get connection to source database
|
||||
sourceDatabase = new MigratorMySQL(logger, settings.sourceHost, settings.sourcePort,
|
||||
settings.sourceDatabase, settings.sourceUsername, settings.sourcePassword, targetCluster);
|
||||
sourceDatabase.load();
|
||||
if (sourceDatabase.isInactive()) {
|
||||
logger.log(Level.WARNING, "Failed to establish connection to the origin MySQL database. " +
|
||||
"Please check you have input the correct connection details and try again.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Carry out the migration
|
||||
public void executeMigrationOperations(DataManager dataManager, HashSet<Server> synchronisedServers, RedisListener redisListener) {
|
||||
// Prepare the target database for insertion
|
||||
prepareTargetDatabase(dataManager);
|
||||
|
||||
// Fetch inventory data from MPDB
|
||||
getInventoryData();
|
||||
|
||||
// Fetch ender chest data from MPDB
|
||||
getEnderChestData();
|
||||
|
||||
// Fetch experience data from MPDB
|
||||
getExperienceData();
|
||||
|
||||
// Send the encoded data to the Bukkit servers for conversion
|
||||
sendEncodedData(synchronisedServers, redisListener);
|
||||
}
|
||||
|
||||
// Clear the new database out of current data
|
||||
private void prepareTargetDatabase(DataManager dataManager) {
|
||||
logger.log(Level.INFO, "Preparing target database...");
|
||||
try (Connection connection = dataManager.getConnection(targetCluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.playerTableName() + ";")) {
|
||||
statement.executeUpdate();
|
||||
}
|
||||
try (PreparedStatement statement = connection.prepareStatement("DELETE FROM " + targetCluster.dataTableName() + ";")) {
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An exception occurred preparing the target database", e);
|
||||
} finally {
|
||||
logger.log(Level.INFO, "Finished preparing target database!");
|
||||
}
|
||||
}
|
||||
|
||||
private void getInventoryData() {
|
||||
logger.log(Level.INFO, "Getting inventory data from MySQLPlayerDataBridge...");
|
||||
try (Connection connection = sourceDatabase.getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.inventoryDataTable + ";")) {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
|
||||
final String playerName = resultSet.getString("player_name");
|
||||
|
||||
MPDBPlayerData data = new MPDBPlayerData(playerUUID, playerName);
|
||||
data.inventoryData = resultSet.getString("inventory");
|
||||
data.armorData = resultSet.getString("armor");
|
||||
|
||||
mpdbPlayerData.add(data);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An exception occurred getting inventory data", e);
|
||||
} finally {
|
||||
logger.log(Level.INFO, "Finished getting inventory data from MySQLPlayerDataBridge");
|
||||
}
|
||||
}
|
||||
|
||||
private void getEnderChestData() {
|
||||
logger.log(Level.INFO, "Getting ender chest data from MySQLPlayerDataBridge...");
|
||||
try (Connection connection = sourceDatabase.getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.enderChestDataTable + ";")) {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
|
||||
|
||||
for (MPDBPlayerData data : mpdbPlayerData) {
|
||||
if (data.playerUUID.equals(playerUUID)) {
|
||||
data.enderChestData = resultSet.getString("enderchest");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An exception occurred getting ender chest data", e);
|
||||
} finally {
|
||||
logger.log(Level.INFO, "Finished getting ender chest data from MySQLPlayerDataBridge");
|
||||
}
|
||||
}
|
||||
|
||||
private void getExperienceData() {
|
||||
logger.log(Level.INFO, "Getting experience data from MySQLPlayerDataBridge...");
|
||||
try (Connection connection = sourceDatabase.getConnection()) {
|
||||
try (PreparedStatement statement = connection.prepareStatement("SELECT * FROM " + migrationSettings.expDataTable + ";")) {
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
while (resultSet.next()) {
|
||||
final UUID playerUUID = UUID.fromString(resultSet.getString("player_uuid"));
|
||||
|
||||
for (MPDBPlayerData data : mpdbPlayerData) {
|
||||
if (data.playerUUID.equals(playerUUID)) {
|
||||
data.expLevel = resultSet.getInt("exp_lvl");
|
||||
data.expProgress = resultSet.getFloat("exp");
|
||||
data.totalExperience = resultSet.getInt("total_exp");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An exception occurred getting experience data", e);
|
||||
} finally {
|
||||
logger.log(Level.INFO, "Finished getting experience data from MySQLPlayerDataBridge");
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEncodedData(HashSet<Server> synchronisedServers, RedisListener redisListener) {
|
||||
for (Server processingServer : synchronisedServers) {
|
||||
if (processingServer.hasMySqlPlayerDataBridge()) {
|
||||
for (MPDBPlayerData data : mpdbPlayerData) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.DECODE_MPDB_DATA,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
processingServer.serverUUID().toString(),
|
||||
RedisMessage.serialize(data))
|
||||
.send();
|
||||
migratedDataSent++;
|
||||
} catch (IOException e) {
|
||||
logger.log(Level.SEVERE, "Failed to serialize encoded MPDB data", e);
|
||||
}
|
||||
}
|
||||
logger.log(Level.INFO, "Finished dispatching encoded data for " + migratedDataSent + " players; please wait for conversion to finish");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all incoming decoded MPDB data to the cache and database
|
||||
*
|
||||
* @param dataToLoad HashMap of the {@link PlayerData} to player Usernames that will be loaded
|
||||
*/
|
||||
public void loadIncomingData(HashMap<PlayerData, String> dataToLoad, DataManager dataManager) {
|
||||
int playersSaved = 0;
|
||||
logger.log(Level.INFO, "Saving data for " + playersMigrated + " players...");
|
||||
|
||||
for (PlayerData playerData : dataToLoad.keySet()) {
|
||||
String playerName = dataToLoad.get(playerData);
|
||||
|
||||
// Add the player to the MySQL table
|
||||
dataManager.ensurePlayerExists(playerData.getPlayerUUID(), playerName);
|
||||
|
||||
// Update the data in the cache and SQL
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
dataManager.updatePlayerData(playerData, cluster);
|
||||
break;
|
||||
}
|
||||
|
||||
playersSaved++;
|
||||
logger.log(Level.INFO, "Saved data for " + playersSaved + "/" + playersMigrated + " players");
|
||||
}
|
||||
|
||||
// Mark as done when done
|
||||
logger.log(Level.INFO, """
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
|
||||
Migration complete!
|
||||
|
||||
Successfully migrated data for %1%/%2% players.
|
||||
|
||||
You should now uninstall MySQLPlayerDataBridge from
|
||||
the rest of the Spigot servers, then restart them.
|
||||
""".replaceAll("%1%", Integer.toString(playersMigrated))
|
||||
.replaceAll("%2%", Integer.toString(migratedDataSent)));
|
||||
sourceDatabase.close(); // Close source database
|
||||
}
|
||||
|
||||
/**
|
||||
* Class used to hold settings for the MPDB migration
|
||||
*/
|
||||
public static class MigrationSettings {
|
||||
public String sourceHost;
|
||||
public int sourcePort;
|
||||
public String sourceDatabase;
|
||||
public String sourceUsername;
|
||||
public String sourcePassword;
|
||||
|
||||
public String inventoryDataTable;
|
||||
public String enderChestDataTable;
|
||||
public String expDataTable;
|
||||
|
||||
public String targetCluster;
|
||||
|
||||
public MigrationSettings() {
|
||||
sourceHost = "localhost";
|
||||
sourcePort = 3306;
|
||||
sourceDatabase = "mpdb";
|
||||
sourceUsername = "root";
|
||||
sourcePassword = "pa55w0rd";
|
||||
|
||||
targetCluster = "main";
|
||||
|
||||
inventoryDataTable = "mpdb_inventory";
|
||||
enderChestDataTable = "mpdb_enderchest";
|
||||
expDataTable = "mpdb_experience";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MySQL class used for importing data from MPDB
|
||||
*/
|
||||
public static class MigratorMySQL extends MySQL {
|
||||
public MigratorMySQL(Logger logger, String host, int port, String database, String username, String password, Settings.SynchronisationCluster cluster) {
|
||||
super(cluster, logger);
|
||||
super.host = host;
|
||||
super.port = port;
|
||||
super.database = database;
|
||||
super.username = username;
|
||||
super.password = password;
|
||||
super.params = "?useSSL=false";
|
||||
super.dataPoolName = super.dataPoolName + "Migrator";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
package net.william278.husksync.migrator;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* A class that stores player data taken from MPDB's database, that can then be converted into HuskSync's format
|
||||
*/
|
||||
public class MPDBPlayerData implements Serializable {
|
||||
|
||||
/*
|
||||
* Player information
|
||||
*/
|
||||
public final UUID playerUUID;
|
||||
public final String playerName;
|
||||
|
||||
/*
|
||||
* Inventory, ender chest and armor data
|
||||
*/
|
||||
public String inventoryData;
|
||||
public String armorData;
|
||||
public String enderChestData;
|
||||
|
||||
/*
|
||||
* Experience data
|
||||
*/
|
||||
public int expLevel;
|
||||
public float expProgress;
|
||||
public int totalExperience;
|
||||
|
||||
public MPDBPlayerData(UUID playerUUID, String playerName) {
|
||||
this.playerUUID = playerUUID;
|
||||
this.playerName = playerName;
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Represents a logged-in {@link User}
|
||||
*/
|
||||
public abstract class OnlineUser extends User {
|
||||
|
||||
public OnlineUser(@NotNull UUID uuid, @NotNull String username) {
|
||||
super(uuid, username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's {@link StatusData}
|
||||
*
|
||||
* @return the player's {@link StatusData}
|
||||
*/
|
||||
public abstract CompletableFuture<StatusData> getStatus();
|
||||
|
||||
/**
|
||||
* Get the player's inventory {@link InventoryData} contents
|
||||
*
|
||||
* @return The player's inventory {@link InventoryData} contents
|
||||
*/
|
||||
public abstract CompletableFuture<InventoryData> getInventory();
|
||||
|
||||
/**
|
||||
* Get the player's ender chest {@link InventoryData} contents
|
||||
*
|
||||
* @return The player's ender chest {@link InventoryData} contents
|
||||
*/
|
||||
public abstract CompletableFuture<InventoryData> getEnderChest();
|
||||
|
||||
/**
|
||||
* Get the player's {@link PotionEffectData}
|
||||
*
|
||||
* @return The player's {@link PotionEffectData}
|
||||
*/
|
||||
public abstract CompletableFuture<PotionEffectData> getPotionEffects();
|
||||
|
||||
/**
|
||||
* Get the player's set of {@link AdvancementData}
|
||||
*
|
||||
* @return the player's set of {@link AdvancementData}
|
||||
*/
|
||||
public abstract CompletableFuture<HashSet<AdvancementData>> getAdvancements();
|
||||
|
||||
/**
|
||||
* Get the player's {@link StatisticsData}
|
||||
*
|
||||
* @return The player's {@link StatisticsData}
|
||||
*/
|
||||
public abstract CompletableFuture<StatisticsData> getStatistics();
|
||||
|
||||
/**
|
||||
* Get the player's {@link LocationData}
|
||||
*
|
||||
* @return the player's {@link LocationData}
|
||||
*/
|
||||
public abstract CompletableFuture<LocationData> getLocation();
|
||||
|
||||
/**
|
||||
* Get the player's {@link PersistentDataContainerData}
|
||||
*
|
||||
* @return The player's {@link PersistentDataContainerData} when fetched
|
||||
*/
|
||||
public abstract CompletableFuture<PersistentDataContainerData> getPersistentDataContainer();
|
||||
|
||||
/**
|
||||
* Set {@link UserData} to a player
|
||||
*
|
||||
* @param data The data to set
|
||||
* @param settings Plugin settings, for determining what needs setting
|
||||
* @return a future that will be completed when done
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setData(@NotNull UserData data, @NotNull Settings settings);
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public abstract void sendMessage(@NotNull MineDown mineDown);
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted action bar message to this player
|
||||
*
|
||||
* @param mineDown the parsed {@link MineDown} to send
|
||||
*/
|
||||
public abstract void sendActionBar(@NotNull MineDown mineDown);
|
||||
|
||||
/**
|
||||
* Returns if the player has the permission node
|
||||
*
|
||||
* @param node The permission node string
|
||||
* @return {@code true} if the player has permission node; {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean hasPermission(@NotNull String node);
|
||||
|
||||
/**
|
||||
* Get the player's current {@link UserData}
|
||||
*
|
||||
* @return the player's current {@link UserData}
|
||||
*/
|
||||
public final CompletableFuture<UserData> getUserData() {
|
||||
return CompletableFuture.supplyAsync(() -> new UserData(getStatus().join(), getInventory().join(),
|
||||
getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(),
|
||||
getStatistics().join(), getLocation().join(), getPersistentDataContainer().join()));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class User {
|
||||
|
||||
@SerializedName("username")
|
||||
public String username;
|
||||
|
||||
@SerializedName("uuid")
|
||||
public UUID uuid;
|
||||
|
||||
public User(@NotNull UUID uuid, @NotNull String username) {
|
||||
this.username = username;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof User other) {
|
||||
return this.uuid.equals(other.uuid);
|
||||
}
|
||||
return super.equals(obj);
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package net.william278.husksync.proxy.command;
|
||||
|
||||
public interface HuskSyncCommand {
|
||||
|
||||
SubCommand[] SUB_COMMANDS = {new SubCommand("about", null),
|
||||
new SubCommand("status", "husksync.command.admin"),
|
||||
new SubCommand("reload", "husksync.command.admin"),
|
||||
new SubCommand("update", "husksync.command.admin"),
|
||||
new SubCommand("invsee", "husksync.command.inventory"),
|
||||
new SubCommand("echest", "husksync.command.ender_chest")};
|
||||
|
||||
/**
|
||||
* A sub command, that may require a permission
|
||||
*/
|
||||
record SubCommand(String command, String permission) { }
|
||||
|
||||
}
|
@ -1,372 +0,0 @@
|
||||
package net.william278.husksync.proxy.data;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.proxy.data.sql.Database;
|
||||
import net.william278.husksync.proxy.data.sql.MySQL;
|
||||
import net.william278.husksync.proxy.data.sql.SQLite;
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class DataManager {
|
||||
|
||||
/**
|
||||
* The player data cache for each cluster ID
|
||||
*/
|
||||
public HashMap<Settings.SynchronisationCluster, PlayerDataCache> playerDataCache = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Map of the database assigned for each cluster
|
||||
*/
|
||||
private final HashMap<String, Database> clusterDatabases;
|
||||
|
||||
// Retrieve database connection for a cluster
|
||||
public Connection getConnection(String clusterId) throws SQLException {
|
||||
return clusterDatabases.get(clusterId).getConnection();
|
||||
}
|
||||
|
||||
// Console logger for errors
|
||||
private final Logger logger;
|
||||
|
||||
// Plugin data folder
|
||||
private final File dataFolder;
|
||||
|
||||
// Flag variable identifying if the data manager failed to initialize
|
||||
public boolean hasFailedInitialization = false;
|
||||
|
||||
public DataManager(Logger logger, File dataFolder) {
|
||||
this.logger = logger;
|
||||
this.dataFolder = dataFolder;
|
||||
clusterDatabases = new HashMap<>();
|
||||
initializeDatabases();
|
||||
}
|
||||
|
||||
private void initializeDatabases() {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
Database clusterDatabase = switch (Settings.dataStorageType) {
|
||||
case SQLITE -> new SQLite(cluster, dataFolder, logger);
|
||||
case MYSQL -> new MySQL(cluster, logger);
|
||||
};
|
||||
clusterDatabase.load();
|
||||
clusterDatabase.createTables();
|
||||
clusterDatabases.put(cluster.clusterId(), clusterDatabase);
|
||||
}
|
||||
|
||||
// Abort loading if the database failed to initialize
|
||||
for (Database database : clusterDatabases.values()) {
|
||||
if (database.isInactive()) {
|
||||
hasFailedInitialization = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connections
|
||||
*/
|
||||
public void closeDatabases() {
|
||||
for (Database database : clusterDatabases.values()) {
|
||||
database.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the player is registered on the database.
|
||||
* If not, register them to the database
|
||||
* If they are, ensure that their player name is up-to-date on the database
|
||||
*
|
||||
* @param playerUUID The UUID of the player to register
|
||||
*/
|
||||
public void ensurePlayerExists(UUID playerUUID, String playerName) {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!playerExists(playerUUID, cluster)) {
|
||||
createPlayerEntry(playerUUID, playerName, cluster);
|
||||
} else {
|
||||
updatePlayerName(playerUUID, playerName, cluster);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the player is registered in SQL (an entry in the PLAYER_TABLE)
|
||||
*
|
||||
* @param playerUUID The UUID of the player
|
||||
* @return {@code true} if the player is on the player table
|
||||
*/
|
||||
private boolean playerExists(UUID playerUUID, Settings.SynchronisationCluster cluster) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM " + cluster.playerTableName() + " WHERE `uuid`=?;")) {
|
||||
statement.setString(1, playerUUID.toString());
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
return resultSet.next();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void createPlayerEntry(UUID playerUUID, String playerName, Settings.SynchronisationCluster cluster) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO " + cluster.playerTableName() + " (`uuid`,`username`) VALUES(?,?);")) {
|
||||
statement.setString(1, playerUUID.toString());
|
||||
statement.setString(2, playerName);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updatePlayerName(UUID playerUUID, String playerName, Settings.SynchronisationCluster cluster) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE " + cluster.playerTableName() + " SET `username`=? WHERE `uuid`=?;")) {
|
||||
statement.setString(1, playerName);
|
||||
statement.setString(2, playerUUID.toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a player's PlayerData by their username
|
||||
*
|
||||
* @param playerName The PlayerName of the data to get
|
||||
* @return Their {@link PlayerData}; or {@code null} if the player does not exist
|
||||
*/
|
||||
public PlayerData getPlayerDataByName(String playerName, String clusterId) {
|
||||
PlayerData playerData = null;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(clusterId)) {
|
||||
try (Connection connection = getConnection(clusterId)) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM " + cluster.playerTableName() + " WHERE `username`=? LIMIT 1;")) {
|
||||
statement.setString(1, playerName);
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final UUID uuid = UUID.fromString(resultSet.getString("uuid"));
|
||||
|
||||
// Get the player data from the cache if it's there, otherwise pull from SQL
|
||||
playerData = playerDataCache.get(cluster).getPlayer(uuid);
|
||||
if (playerData == null) {
|
||||
playerData = Objects.requireNonNull(getPlayerData(uuid)).get(cluster);
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
return playerData;
|
||||
}
|
||||
|
||||
public Map<Settings.SynchronisationCluster, PlayerData> getPlayerData(UUID playerUUID) {
|
||||
HashMap<Settings.SynchronisationCluster, PlayerData> data = new HashMap<>();
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM " + cluster.dataTableName() + " WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
|
||||
statement.setString(1, playerUUID.toString());
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
if (resultSet.next()) {
|
||||
final UUID dataVersionUUID = UUID.fromString(resultSet.getString("version_uuid"));
|
||||
final Timestamp dataSaveTimestamp = resultSet.getTimestamp("timestamp");
|
||||
final String serializedInventory = resultSet.getString("inventory");
|
||||
final String serializedEnderChest = resultSet.getString("ender_chest");
|
||||
final double health = resultSet.getDouble("health");
|
||||
final double maxHealth = resultSet.getDouble("max_health");
|
||||
final double healthScale = resultSet.getDouble("health_scale");
|
||||
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");
|
||||
final int totalExperience = resultSet.getInt("total_experience");
|
||||
final int expLevel = resultSet.getInt("exp_level");
|
||||
final float expProgress = resultSet.getFloat("exp_progress");
|
||||
final String gameMode = resultSet.getString("game_mode");
|
||||
final boolean isFlying = resultSet.getBoolean("is_flying");
|
||||
final String serializedAdvancementData = resultSet.getString("advancements");
|
||||
final String serializedLocationData = resultSet.getString("location");
|
||||
final String serializedStatisticData = resultSet.getString("statistics");
|
||||
|
||||
data.put(cluster, new PlayerData(playerUUID, dataVersionUUID, dataSaveTimestamp.toInstant().getEpochSecond(),
|
||||
serializedInventory, serializedEnderChest, health, maxHealth, healthScale, hunger, saturation,
|
||||
saturationExhaustion, selectedSlot, serializedStatusEffects, totalExperience, expLevel, expProgress,
|
||||
gameMode, serializedStatisticData, isFlying, serializedAdvancementData, serializedLocationData));
|
||||
} else {
|
||||
data.put(cluster, PlayerData.DEFAULT_PLAYER_DATA(playerUUID));
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
public void updatePlayerData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
|
||||
// Ignore if the Spigot server didn't properly sync the previous data
|
||||
|
||||
// Add the new player data to the cache
|
||||
playerDataCache.get(cluster).updatePlayer(playerData);
|
||||
|
||||
// SQL: If the player has cached data, update it, otherwise insert new data.
|
||||
if (playerHasCachedData(playerData.getPlayerUUID(), cluster)) {
|
||||
updatePlayerSQLData(playerData, cluster);
|
||||
} else {
|
||||
insertPlayerData(playerData, cluster);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlayerSQLData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"UPDATE " + cluster.dataTableName() + " SET `version_uuid`=?, `timestamp`=?, `inventory`=?, `ender_chest`=?, `health`=?, `max_health`=?, `health_scale`=?, `hunger`=?, `saturation`=?, `saturation_exhaustion`=?, `selected_slot`=?, `status_effects`=?, `total_experience`=?, `exp_level`=?, `exp_progress`=?, `game_mode`=?, `statistics`=?, `is_flying`=?, `advancements`=?, `location`=? WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
|
||||
statement.setString(1, playerData.getDataVersionUUID().toString());
|
||||
statement.setTimestamp(2, new Timestamp(System.currentTimeMillis()));
|
||||
statement.setString(3, playerData.getSerializedInventory());
|
||||
statement.setString(4, playerData.getSerializedEnderChest());
|
||||
statement.setDouble(5, playerData.getHealth()); // Health
|
||||
statement.setDouble(6, playerData.getMaxHealth()); // Max health
|
||||
statement.setDouble(7, playerData.getHealthScale()); // Health scale
|
||||
statement.setInt(8, playerData.getHunger()); // Hunger
|
||||
statement.setFloat(9, playerData.getSaturation()); // Saturation
|
||||
statement.setFloat(10, playerData.getSaturationExhaustion()); // Saturation exhaustion
|
||||
statement.setInt(11, playerData.getSelectedSlot()); // Current selected slot
|
||||
statement.setString(12, playerData.getSerializedEffectData()); // Status effects
|
||||
statement.setInt(13, playerData.getTotalExperience()); // Total Experience
|
||||
statement.setInt(14, playerData.getExpLevel()); // Exp level
|
||||
statement.setFloat(15, playerData.getExpProgress()); // Exp progress
|
||||
statement.setString(16, playerData.getGameMode()); // GameMode
|
||||
statement.setString(17, playerData.getSerializedStatistics()); // Statistics
|
||||
statement.setBoolean(18, playerData.isFlying()); // Is flying
|
||||
statement.setString(19, playerData.getSerializedAdvancements()); // Advancements
|
||||
statement.setString(20, playerData.getSerializedLocation()); // Location
|
||||
|
||||
statement.setString(21, playerData.getPlayerUUID().toString());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertPlayerData(PlayerData playerData, Settings.SynchronisationCluster cluster) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"INSERT INTO " + cluster.dataTableName() + " (`player_id`,`version_uuid`,`timestamp`,`inventory`,`ender_chest`,`health`,`max_health`,`health_scale`,`hunger`,`saturation`,`saturation_exhaustion`,`selected_slot`,`status_effects`,`total_experience`,`exp_level`,`exp_progress`,`game_mode`,`statistics`,`is_flying`,`advancements`,`location`) VALUES((SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);")) {
|
||||
statement.setString(1, playerData.getPlayerUUID().toString());
|
||||
statement.setString(2, playerData.getDataVersionUUID().toString());
|
||||
statement.setTimestamp(3, new Timestamp(System.currentTimeMillis()));
|
||||
statement.setString(4, playerData.getSerializedInventory());
|
||||
statement.setString(5, playerData.getSerializedEnderChest());
|
||||
statement.setDouble(6, playerData.getHealth()); // Health
|
||||
statement.setDouble(7, playerData.getMaxHealth()); // Max health
|
||||
statement.setDouble(8, playerData.getHealthScale()); // Health scale
|
||||
statement.setInt(9, playerData.getHunger()); // Hunger
|
||||
statement.setFloat(10, playerData.getSaturation()); // Saturation
|
||||
statement.setFloat(11, playerData.getSaturationExhaustion()); // Saturation exhaustion
|
||||
statement.setInt(12, playerData.getSelectedSlot()); // Current selected slot
|
||||
statement.setString(13, playerData.getSerializedEffectData()); // Status effects
|
||||
statement.setInt(14, playerData.getTotalExperience()); // Total Experience
|
||||
statement.setInt(15, playerData.getExpLevel()); // Exp level
|
||||
statement.setFloat(16, playerData.getExpProgress()); // Exp progress
|
||||
statement.setString(17, playerData.getGameMode()); // GameMode
|
||||
statement.setString(18, playerData.getSerializedStatistics()); // Statistics
|
||||
statement.setBoolean(19, playerData.isFlying()); // Is flying
|
||||
statement.setString(20, playerData.getSerializedAdvancements()); // Advancements
|
||||
statement.setString(21, playerData.getSerializedLocation()); // Location
|
||||
|
||||
statement.executeUpdate();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the player has cached data saved in SQL (an entry in the DATA_TABLE)
|
||||
*
|
||||
* @param playerUUID The UUID of the player
|
||||
* @return {@code true} if the player has an entry in the data table
|
||||
*/
|
||||
private boolean playerHasCachedData(UUID playerUUID, Settings.SynchronisationCluster cluster) {
|
||||
try (Connection connection = getConnection(cluster.clusterId())) {
|
||||
try (PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM " + cluster.dataTableName() + " WHERE `player_id`=(SELECT `id` FROM " + cluster.playerTableName() + " WHERE `uuid`=?);")) {
|
||||
statement.setString(1, playerUUID.toString());
|
||||
ResultSet resultSet = statement.executeQuery();
|
||||
return resultSet.next();
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An SQL exception occurred", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A cache of PlayerData
|
||||
*/
|
||||
public static class PlayerDataCache {
|
||||
// The cached player data
|
||||
public HashSet<PlayerData> playerData;
|
||||
|
||||
public PlayerDataCache() {
|
||||
playerData = new HashSet<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update ar add data for a player to the cache
|
||||
*
|
||||
* @param newData The player's new/updated {@link PlayerData}
|
||||
*/
|
||||
public void updatePlayer(PlayerData newData) {
|
||||
// Remove the old data if it exists
|
||||
PlayerData oldData = null;
|
||||
for (PlayerData data : playerData) {
|
||||
if (data.getPlayerUUID().equals(newData.getPlayerUUID())) {
|
||||
oldData = data;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (oldData != null) {
|
||||
playerData.remove(oldData);
|
||||
}
|
||||
|
||||
// Add the new data
|
||||
playerData.add(newData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a player's {@link PlayerData} by their {@link UUID}
|
||||
*
|
||||
* @param playerUUID The {@link UUID} of the player to check
|
||||
* @return The player's {@link PlayerData}
|
||||
*/
|
||||
public PlayerData getPlayer(UUID playerUUID) {
|
||||
for (PlayerData data : playerData) {
|
||||
if (data.getPlayerUUID().equals(playerUUID)) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package net.william278.husksync.proxy.data.sql;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public abstract class Database {
|
||||
|
||||
public String dataPoolName;
|
||||
public Settings.SynchronisationCluster cluster;
|
||||
public final Logger logger;
|
||||
|
||||
public Database(Settings.SynchronisationCluster cluster, Logger logger) {
|
||||
this.cluster = cluster;
|
||||
this.dataPoolName = cluster != null ? "HuskSyncHikariPool-" + cluster.clusterId() : "HuskSyncMigratorPool";
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public abstract Connection getConnection() throws SQLException;
|
||||
|
||||
public boolean isInactive() {
|
||||
try {
|
||||
return getConnection() == null;
|
||||
} catch (SQLException e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void load();
|
||||
|
||||
public abstract void createTables();
|
||||
|
||||
public abstract void close();
|
||||
|
||||
public final int hikariMaximumPoolSize = Settings.hikariMaximumPoolSize;
|
||||
public final int hikariMinimumIdle = Settings.hikariMinimumIdle;
|
||||
public final long hikariMaximumLifetime = Settings.hikariMaximumLifetime;
|
||||
public final long hikariKeepAliveTime = Settings.hikariKeepAliveTime;
|
||||
public final long hikariConnectionTimeOut = Settings.hikariConnectionTimeOut;
|
||||
}
|
@ -1,113 +0,0 @@
|
||||
package net.william278.husksync.proxy.data.sql;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class MySQL extends Database {
|
||||
|
||||
final String[] SQL_SETUP_STATEMENTS = {
|
||||
"CREATE TABLE IF NOT EXISTS " + cluster.playerTableName() + " (" +
|
||||
"`id` integer NOT NULL AUTO_INCREMENT," +
|
||||
"`uuid` char(36) NOT NULL UNIQUE," +
|
||||
"`username` varchar(16) NOT NULL," +
|
||||
|
||||
"PRIMARY KEY (`id`)" +
|
||||
");",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " + cluster.dataTableName() + " (" +
|
||||
"`player_id` integer NOT NULL," +
|
||||
"`version_uuid` char(36) NOT NULL UNIQUE," +
|
||||
"`timestamp` datetime NOT NULL," +
|
||||
"`inventory` longtext NOT NULL," +
|
||||
"`ender_chest` longtext NOT NULL," +
|
||||
"`health` double NOT NULL," +
|
||||
"`max_health` double NOT NULL," +
|
||||
"`health_scale` 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," +
|
||||
"`total_experience` integer NOT NULL," +
|
||||
"`exp_level` integer NOT NULL," +
|
||||
"`exp_progress` float NOT NULL," +
|
||||
"`game_mode` tinytext NOT NULL," +
|
||||
"`statistics` longtext NOT NULL," +
|
||||
"`is_flying` boolean NOT NULL," +
|
||||
"`advancements` longtext NOT NULL," +
|
||||
"`location` text NOT NULL," +
|
||||
|
||||
"PRIMARY KEY (`player_id`,`version_uuid`)," +
|
||||
"FOREIGN KEY (`player_id`) REFERENCES " + cluster.playerTableName() + " (`id`)" +
|
||||
");"
|
||||
|
||||
};
|
||||
|
||||
public String host = Settings.mySQLHost;
|
||||
public int port = Settings.mySQLPort;
|
||||
public String database = Settings.mySQLDatabase;
|
||||
public String username = Settings.mySQLUsername;
|
||||
public String password = Settings.mySQLPassword;
|
||||
public String params = Settings.mySQLParams;
|
||||
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
public MySQL(Settings.SynchronisationCluster cluster, Logger logger) {
|
||||
super(cluster, logger);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
// Create new HikariCP data source
|
||||
final String jdbcUrl = "jdbc:mysql://" + host + ":" + port + "/" + database + params;
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl(jdbcUrl);
|
||||
|
||||
dataSource.setUsername(username);
|
||||
dataSource.setPassword(password);
|
||||
|
||||
// Set data source driver path
|
||||
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
|
||||
|
||||
// Set various additional parameters
|
||||
dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
|
||||
dataSource.setMinimumIdle(hikariMinimumIdle);
|
||||
dataSource.setMaxLifetime(hikariMaximumLifetime);
|
||||
dataSource.setKeepaliveTime(hikariKeepAliveTime);
|
||||
dataSource.setConnectionTimeout(hikariConnectionTimeOut);
|
||||
dataSource.setPoolName(dataPoolName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createTables() {
|
||||
// Create tables
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : SQL_SETUP_STATEMENTS) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package net.william278.husksync.proxy.data.sql;
|
||||
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class SQLite extends Database {
|
||||
|
||||
final String[] SQL_SETUP_STATEMENTS = {
|
||||
"PRAGMA foreign_keys = ON;",
|
||||
"PRAGMA encoding = 'UTF-8';",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " + cluster.playerTableName() + " (" +
|
||||
"`id` integer PRIMARY KEY," +
|
||||
"`uuid` char(36) NOT NULL UNIQUE," +
|
||||
"`username` varchar(16) NOT NULL" +
|
||||
");",
|
||||
|
||||
"CREATE TABLE IF NOT EXISTS " + cluster.dataTableName() + " (" +
|
||||
"`player_id` integer NOT NULL REFERENCES " + cluster.playerTableName() + "(`id`)," +
|
||||
"`version_uuid` char(36) NOT NULL UNIQUE," +
|
||||
"`timestamp` datetime NOT NULL," +
|
||||
"`inventory` longtext NOT NULL," +
|
||||
"`ender_chest` longtext NOT NULL," +
|
||||
"`health` double NOT NULL," +
|
||||
"`max_health` double NOT NULL," +
|
||||
"`health_scale` 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," +
|
||||
"`total_experience` integer NOT NULL," +
|
||||
"`exp_level` integer NOT NULL," +
|
||||
"`exp_progress` float NOT NULL," +
|
||||
"`game_mode` tinytext NOT NULL," +
|
||||
"`statistics` longtext NOT NULL," +
|
||||
"`is_flying` boolean NOT NULL," +
|
||||
"`advancements` longtext NOT NULL," +
|
||||
"`location` text NOT NULL," +
|
||||
|
||||
"PRIMARY KEY (`player_id`,`version_uuid`)" +
|
||||
");"
|
||||
};
|
||||
|
||||
private String getDatabaseName() {
|
||||
return cluster.databaseName() + "Data";
|
||||
}
|
||||
|
||||
private final File dataFolder;
|
||||
|
||||
private HikariDataSource dataSource;
|
||||
|
||||
public SQLite(Settings.SynchronisationCluster cluster, File dataFolder, Logger logger) {
|
||||
super(cluster, logger);
|
||||
this.dataFolder = dataFolder;
|
||||
}
|
||||
|
||||
// Create the database file if it does not exist yet
|
||||
private void createDatabaseFileIfNotExist() {
|
||||
File databaseFile = new File(dataFolder, getDatabaseName() + ".db");
|
||||
if (!databaseFile.exists()) {
|
||||
try {
|
||||
if (!databaseFile.createNewFile()) {
|
||||
logger.log(Level.SEVERE, "Failed to write new file: " + getDatabaseName() + ".db (file already exists)");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
logger.log(Level.SEVERE, "An error occurred writing a file: " + getDatabaseName() + ".db (" + e.getCause() + ")", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Connection getConnection() throws SQLException {
|
||||
return dataSource.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
// Make SQLite database file
|
||||
createDatabaseFileIfNotExist();
|
||||
|
||||
// Create new HikariCP data source
|
||||
final String jdbcUrl = "jdbc:sqlite:" + dataFolder.getAbsolutePath() + File.separator + getDatabaseName() + ".db";
|
||||
dataSource = new HikariDataSource();
|
||||
dataSource.setDataSourceClassName("org.sqlite.SQLiteDataSource");
|
||||
dataSource.addDataSourceProperty("url", jdbcUrl);
|
||||
|
||||
// Set various additional parameters
|
||||
dataSource.setMaximumPoolSize(hikariMaximumPoolSize);
|
||||
dataSource.setMinimumIdle(hikariMinimumIdle);
|
||||
dataSource.setMaxLifetime(hikariMaximumLifetime);
|
||||
dataSource.setKeepaliveTime(hikariKeepAliveTime);
|
||||
dataSource.setConnectionTimeout(hikariConnectionTimeOut);
|
||||
dataSource.setPoolName(dataPoolName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createTables() {
|
||||
// Create tables
|
||||
try (Connection connection = dataSource.getConnection()) {
|
||||
try (Statement statement = connection.createStatement()) {
|
||||
for (String tableCreationStatement : SQL_SETUP_STATEMENTS) {
|
||||
statement.execute(tableCreationStatement);
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.log(Level.SEVERE, "An error occurred creating tables on the SQLite database", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,126 +0,0 @@
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import net.william278.husksync.Settings;
|
||||
import redis.clients.jedis.*;
|
||||
import redis.clients.jedis.exceptions.JedisConnectionException;
|
||||
import redis.clients.jedis.exceptions.JedisException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public abstract class RedisListener {
|
||||
|
||||
/**
|
||||
* Determines if the RedisListener is working properly
|
||||
*/
|
||||
public boolean isActiveAndEnabled;
|
||||
|
||||
/**
|
||||
* Pool of connections to the Redis server
|
||||
*/
|
||||
private static JedisPool jedisPool;
|
||||
|
||||
/**
|
||||
* Creates a new RedisListener and initialises the Redis connection
|
||||
*/
|
||||
public RedisListener() {
|
||||
JedisPoolConfig config = new JedisPoolConfig();
|
||||
config.setMaxIdle(0);
|
||||
config.setTestOnBorrow(true);
|
||||
config.setTestOnReturn(true);
|
||||
if (Settings.redisPassword.isEmpty()) {
|
||||
jedisPool = new JedisPool(config,
|
||||
Settings.redisHost,
|
||||
Settings.redisPort,
|
||||
0,
|
||||
Settings.redisSSL);
|
||||
} else {
|
||||
jedisPool = new JedisPool(config,
|
||||
Settings.redisHost,
|
||||
Settings.redisPort,
|
||||
0,
|
||||
Settings.redisPassword,
|
||||
Settings.redisSSL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
public abstract void handleMessage(RedisMessage message);
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
public abstract void log(Level level, String message);
|
||||
|
||||
/**
|
||||
* Fetch a connection to the Redis server from the JedisPool
|
||||
*
|
||||
* @return Jedis instance from the pool
|
||||
*/
|
||||
public static Jedis getJedisConnection() {
|
||||
return jedisPool.getResource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the Redis listener
|
||||
*/
|
||||
public final void listen() {
|
||||
new Thread(() -> {
|
||||
isActiveAndEnabled = true;
|
||||
while (isActiveAndEnabled) {
|
||||
|
||||
Jedis subscriber;
|
||||
if (Settings.redisPassword.isEmpty()) {
|
||||
subscriber = new Jedis(Settings.redisHost,
|
||||
Settings.redisPort,
|
||||
0);
|
||||
} else {
|
||||
final JedisClientConfig config = DefaultJedisClientConfig.builder()
|
||||
.password(Settings.redisPassword)
|
||||
.timeoutMillis(0).build();
|
||||
|
||||
subscriber = new Jedis(Settings.redisHost,
|
||||
Settings.redisPort,
|
||||
config);
|
||||
}
|
||||
subscriber.connect();
|
||||
|
||||
log(Level.INFO, "Enabled Redis listener successfully!");
|
||||
try {
|
||||
subscriber.subscribe(new JedisPubSub() {
|
||||
@Override
|
||||
public void onMessage(String channel, String message) {
|
||||
// Only accept messages to the HuskSync channel
|
||||
if (!channel.equals(RedisMessage.REDIS_CHANNEL)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle the message
|
||||
try {
|
||||
handleMessage(new RedisMessage(message));
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize message target");
|
||||
}
|
||||
}
|
||||
}, RedisMessage.REDIS_CHANNEL);
|
||||
} catch (JedisConnectionException connectionException) {
|
||||
log(Level.SEVERE, "A connection exception occurred with the Jedis listener");
|
||||
connectionException.printStackTrace();
|
||||
} catch (JedisException jedisException) {
|
||||
isActiveAndEnabled = false;
|
||||
log(Level.SEVERE, "An exception occurred with the Jedis listener");
|
||||
jedisException.printStackTrace();
|
||||
} finally {
|
||||
subscriber.close();
|
||||
}
|
||||
}
|
||||
}, "Redis Subscriber").start();
|
||||
}
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import redis.clients.jedis.Jedis;
|
||||
import redis.clients.jedis.JedisPool;
|
||||
import redis.clients.jedis.JedisPoolConfig;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class RedisManager {
|
||||
|
||||
private static final String KEY_NAMESPACE = "husksync:";
|
||||
private static String clusterId = "";
|
||||
private final JedisPool jedisPool;
|
||||
|
||||
private RedisManager(@NotNull Settings settings) {
|
||||
clusterId = settings.getStringValue(Settings.ConfigOption.CLUSTER_ID);
|
||||
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
|
||||
jedisPoolConfig.setMaxIdle(0);
|
||||
jedisPoolConfig.setTestOnBorrow(true);
|
||||
jedisPoolConfig.setTestOnReturn(true);
|
||||
if (settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD).isBlank()) {
|
||||
jedisPool = new JedisPool(jedisPoolConfig,
|
||||
settings.getStringValue(Settings.ConfigOption.REDIS_HOST),
|
||||
settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT),
|
||||
0,
|
||||
settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL));
|
||||
} else {
|
||||
jedisPool = new JedisPool(jedisPoolConfig,
|
||||
settings.getStringValue(Settings.ConfigOption.REDIS_HOST),
|
||||
settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT),
|
||||
0,
|
||||
settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD),
|
||||
settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL));
|
||||
}
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> setPlayerData(@NotNull User user, @NotNull UserData userData,
|
||||
@NotNull RedisKeyType redisKeyType) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
jedis.setex(redisKeyType.getKeyPrefix() + user.uuid.toString(),
|
||||
redisKeyType.timeToLive, userData.toJson());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user, @NotNull RedisKeyType redisKeyType) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final String json = jedis.get(redisKeyType.getKeyPrefix() + user.uuid.toString());
|
||||
if (json == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(UserData.fromJson(json));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static CompletableFuture<RedisManager> initialize(@NotNull Settings settings) {
|
||||
return CompletableFuture.supplyAsync(() -> new RedisManager(settings));
|
||||
}
|
||||
|
||||
public enum RedisKeyType {
|
||||
CACHE(60 * 60 * 24),
|
||||
SERVER_CHANGE(2);
|
||||
|
||||
public final int timeToLive;
|
||||
|
||||
RedisKeyType(int timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getKeyPrefix() {
|
||||
return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase() + ":";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
package net.william278.husksync.redis;
|
||||
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.Base64;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
|
||||
public class RedisMessage {
|
||||
|
||||
public static String REDIS_CHANNEL = "HuskSync";
|
||||
|
||||
public static String MESSAGE_META_SEPARATOR = "♦";
|
||||
public static String MESSAGE_DATA_SEPARATOR = "♣";
|
||||
|
||||
private final String messageData;
|
||||
private final MessageType messageType;
|
||||
private final MessageTarget messageTarget;
|
||||
|
||||
/**
|
||||
* Create a new RedisMessage
|
||||
*
|
||||
* @param type The type of the message
|
||||
* @param target Who will receive this message
|
||||
* @param messageData The message data elements
|
||||
*/
|
||||
public RedisMessage(MessageType type, MessageTarget target, String... messageData) {
|
||||
final StringJoiner messageDataJoiner = new StringJoiner(MESSAGE_DATA_SEPARATOR);
|
||||
for (String dataElement : messageData) {
|
||||
messageDataJoiner.add(dataElement);
|
||||
}
|
||||
this.messageData = messageDataJoiner.toString();
|
||||
this.messageType = type;
|
||||
this.messageTarget = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new RedisMessage from an incoming message string
|
||||
*
|
||||
* @param messageString The message string to parse
|
||||
*/
|
||||
public RedisMessage(String messageString) throws IOException, ClassNotFoundException {
|
||||
String[] messageMetaElements = messageString.split(MESSAGE_META_SEPARATOR);
|
||||
messageType = MessageType.valueOf(messageMetaElements[0]);
|
||||
messageTarget = (MessageTarget) RedisMessage.deserialize(messageMetaElements[1]);
|
||||
messageData = messageMetaElements[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the full, formatted message string with type, target & data
|
||||
*
|
||||
* @return The fully formatted message
|
||||
*/
|
||||
private String getFullMessage() throws IOException {
|
||||
return new StringJoiner(MESSAGE_META_SEPARATOR)
|
||||
.add(messageType.toString()).add(RedisMessage.serialize(messageTarget)).add(messageData)
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the redis message
|
||||
*/
|
||||
public void send() throws IOException {
|
||||
try (Jedis publisher = RedisListener.getJedisConnection()) {
|
||||
publisher.publish(REDIS_CHANNEL, getFullMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public String getMessageData() {
|
||||
return messageData;
|
||||
}
|
||||
|
||||
public String[] getMessageDataElements() {
|
||||
return messageData.split(MESSAGE_DATA_SEPARATOR);
|
||||
}
|
||||
|
||||
public MessageType getMessageType() {
|
||||
return messageType;
|
||||
}
|
||||
|
||||
public MessageTarget getMessageTarget() {
|
||||
return messageTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the type of the message
|
||||
*/
|
||||
public enum MessageType implements Serializable {
|
||||
/**
|
||||
* Sent by Bukkit servers to proxy when a user disconnects with that player's updated {@link PlayerData}.
|
||||
*/
|
||||
PLAYER_DATA_UPDATE,
|
||||
|
||||
/**
|
||||
* Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy if they are set as needing to request data on join.
|
||||
*/
|
||||
PLAYER_DATA_REQUEST,
|
||||
|
||||
/**
|
||||
* Sent by the Proxy to reply to a {@code MessageType.PLAYER_DATA_REQUEST}, contains the latest {@link PlayerData} for the requester.
|
||||
*/
|
||||
PLAYER_DATA_SET,
|
||||
|
||||
/**
|
||||
* Sent by Bukkit servers to proxy to request {@link PlayerData} from the proxy via the API.
|
||||
*/
|
||||
API_DATA_REQUEST,
|
||||
|
||||
/**
|
||||
* Sent by the Proxy to fulfill an {@code MessageType.API_DATA_REQUEST}, containing the latest {@link PlayerData} for the requested UUID.
|
||||
*/
|
||||
API_DATA_RETURN,
|
||||
|
||||
/**
|
||||
* Sent by the Proxy to cancel an {@code MessageType.API_DATA_REQUEST} if no data can be returned.
|
||||
*/
|
||||
API_DATA_CANCEL,
|
||||
|
||||
/**
|
||||
* Sent by the proxy to a Bukkit server to have them request data on join; contains no data otherwise.
|
||||
*/
|
||||
REQUEST_DATA_ON_JOIN,
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
/**
|
||||
* Sent by the proxy to show a player the contents of another player's inventory, contains their username and {@link PlayerData}.
|
||||
*/
|
||||
OPEN_INVENTORY,
|
||||
|
||||
/**
|
||||
* Sent by the proxy to show a player the contents of another player's ender chest, contains their username and {@link PlayerData}.
|
||||
*/
|
||||
OPEN_ENDER_CHEST,
|
||||
|
||||
/**
|
||||
* Sent by both the proxy and bukkit servers to confirm cross-server communication has been established.
|
||||
*/
|
||||
CONNECTION_HANDSHAKE,
|
||||
|
||||
/**
|
||||
* Sent by both the proxy and bukkit servers to terminate communications (if a bukkit / the proxy goes offline).
|
||||
*/
|
||||
TERMINATE_HANDSHAKE,
|
||||
|
||||
/**
|
||||
* Sent by a proxy to a bukkit server to decode MPDB data.
|
||||
*/
|
||||
DECODE_MPDB_DATA,
|
||||
|
||||
/**
|
||||
* Sent by a bukkit server back to the proxy with the correctly decoded MPDB data.
|
||||
*/
|
||||
DECODED_MPDB_DATA_SET,
|
||||
|
||||
/**
|
||||
* Sent by the proxy to a bukkit server to initiate a reload.
|
||||
*/
|
||||
RELOAD_CONFIG
|
||||
}
|
||||
|
||||
public enum RequestOnJoinUpdateType {
|
||||
ADD_REQUESTER,
|
||||
REMOVE_REQUESTER
|
||||
}
|
||||
|
||||
/**
|
||||
* A record that defines the target of a plugin message; a spigot server or the proxy server(s). For Bukkit servers, the name of the server must also be specified
|
||||
*/
|
||||
public record MessageTarget(Settings.ServerType targetServerType, UUID targetPlayerUUID,
|
||||
String targetClusterId) implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize an object from a Base64 string
|
||||
*/
|
||||
public static Object deserialize(String s) throws IOException, ClassNotFoundException {
|
||||
byte[] data = Base64.getDecoder().decode(s);
|
||||
try (ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(data))) {
|
||||
return objectInputStream.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an object to a Base64 string
|
||||
*/
|
||||
public static String serialize(Serializable o) throws IOException {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
|
||||
objectOutputStream.writeObject(o);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class MessageManager {
|
||||
|
||||
private static HashMap<String, String> messages = new HashMap<>();
|
||||
|
||||
public static void setMessages(HashMap<String, String> newMessages) {
|
||||
messages = new HashMap<>(newMessages);
|
||||
}
|
||||
|
||||
public static String getMessage(String messageId) {
|
||||
return messages.get(messageId);
|
||||
}
|
||||
|
||||
public static StringBuilder PLUGIN_INFORMATION = new StringBuilder().append("[HuskSync](#00fb9a bold) [| %proxy_brand% Version %proxy_version% (%bukkit_brand% v%bukkit_version%)](#00fb9a)\n")
|
||||
.append("[%plugin_description%](gray)\n")
|
||||
.append("[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)\n")
|
||||
.append("[• Contributors:](white) [HarvelsX](gray show_text=&7Code)\n")
|
||||
.append("[• Translators:](white) [Namiu/うにたろう](gray show_text=&7Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua)\n")
|
||||
.append("[• Plugin Info:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/)\n")
|
||||
.append("[• Report Issues:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)\n")
|
||||
.append("[• Support Discord:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)");
|
||||
|
||||
public static StringBuilder PLUGIN_STATUS = new StringBuilder().append("[HuskSync](#00fb9a bold) [| Current system status:](#00fb9a)\n")
|
||||
.append("[• Connected servers:](white) [%1%](#00fb9a)\n")
|
||||
.append("[• Cached player data:](white) [%2%](#00fb9a)");
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Abstract representation of a reader that reads internal resource files by name
|
||||
*/
|
||||
public interface ResourceReader {
|
||||
|
||||
/**
|
||||
* Gets the resource with given filename and reads it as an {@link InputStream}
|
||||
*
|
||||
* @param fileName Name of the resource file to read
|
||||
* @return The resource, read as an {@link InputStream}
|
||||
*/
|
||||
@NotNull InputStream getResource(String fileName);
|
||||
|
||||
/**
|
||||
* Gets the plugin data folder where plugin configuration and data are kept
|
||||
*
|
||||
* @return the plugin data directory
|
||||
*/
|
||||
@NotNull File getDataFolder();
|
||||
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
public interface ThrowSupplier<T> {
|
||||
T get() throws Exception;
|
||||
|
||||
static <A> A get(ThrowSupplier<A> supplier) {
|
||||
try {
|
||||
return supplier.get();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,53 +1,59 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public abstract class UpdateChecker {
|
||||
public class UpdateChecker {
|
||||
|
||||
private final static int SPIGOT_PROJECT_ID = 97144;
|
||||
|
||||
private final Logger logger;
|
||||
private final VersionUtils.Version currentVersion;
|
||||
private VersionUtils.Version latestVersion;
|
||||
|
||||
public UpdateChecker(String currentVersion) {
|
||||
public UpdateChecker(@NotNull String currentVersion, @NotNull Logger logger) {
|
||||
this.currentVersion = VersionUtils.Version.of(currentVersion);
|
||||
|
||||
try {
|
||||
final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
|
||||
URLConnection urlConnection = url.openConnection();
|
||||
this.latestVersion = VersionUtils.Version.of(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
|
||||
} catch (IOException e) {
|
||||
log(Level.WARNING, "Failed to check for updates: An IOException occurred.");
|
||||
this.latestVersion = new VersionUtils.Version();
|
||||
} catch (Exception e) {
|
||||
log(Level.WARNING, "Failed to check for updates: An exception occurred.");
|
||||
this.latestVersion = new VersionUtils.Version();
|
||||
}
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public boolean isUpToDate() {
|
||||
return this.currentVersion.compareTo(latestVersion) >= 0;
|
||||
public CompletableFuture<VersionUtils.Version> fetchLatestVersion() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
|
||||
URLConnection urlConnection = url.openConnection();
|
||||
return VersionUtils.Version.of(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "Failed to fetch the latest plugin version", e);
|
||||
}
|
||||
return new VersionUtils.Version();
|
||||
});
|
||||
}
|
||||
|
||||
public String getLatestVersion() {
|
||||
return latestVersion.toString();
|
||||
public boolean isUpdateAvailable(@NotNull VersionUtils.Version latestVersion) {
|
||||
return latestVersion.compareTo(currentVersion) > 0;
|
||||
}
|
||||
|
||||
public String getCurrentVersion() {
|
||||
return currentVersion.toString();
|
||||
public VersionUtils.Version getCurrentVersion() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public abstract void log(Level level, String message);
|
||||
public CompletableFuture<Boolean> isUpToDate() {
|
||||
return fetchLatestVersion().thenApply(this::isUpdateAvailable);
|
||||
}
|
||||
|
||||
public void logToConsole() {
|
||||
if (!isUpToDate()) {
|
||||
log(Level.WARNING, "A new version of HuskSync is available: Version "
|
||||
+ latestVersion + " (Currently running: " + currentVersion + ")");
|
||||
}
|
||||
fetchLatestVersion().thenAccept(latestVersion -> {
|
||||
if (isUpdateAvailable(latestVersion)) {
|
||||
logger.log(Level.WARNING, "A new version of HuskSync is available: v" + latestVersion);
|
||||
} else {
|
||||
logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + currentVersion + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
# ------------------------------
|
||||
# | HuskSync Config |
|
||||
# | Developed by William278 |
|
||||
# ------------------------------
|
||||
# Documentation available at: https://william278.net/docs/husksync/Setup
|
||||
|
||||
language: 'en-gb'
|
||||
check_for_updates: true
|
||||
cluster_id: ''
|
||||
|
||||
database:
|
||||
credentials:
|
||||
host: 'localhost'
|
||||
port: 3306
|
||||
database: 'HuskSync'
|
||||
username: 'root'
|
||||
password: 'pa55w0rd'
|
||||
params: '?autoReconnect=true&useSSL=false'
|
||||
connection_pool:
|
||||
maximum_pool_size: 10
|
||||
minimum_idle: 10
|
||||
maximum_lifetime: 1800000
|
||||
keepalive_time: 0
|
||||
connection_timeout: 5000
|
||||
table_names:
|
||||
players_table: 'husksync_players'
|
||||
data_table: 'husksync_data'
|
||||
|
||||
redis:
|
||||
credentials:
|
||||
host: 'localhost'
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
|
||||
synchronization:
|
||||
max_user_data_records: 5
|
||||
save_on_world_save: true
|
||||
features:
|
||||
inventories: true
|
||||
ender_chests: true
|
||||
health: true
|
||||
max_health: true
|
||||
hunger: true
|
||||
experience: true
|
||||
potion_effects: true
|
||||
advancements: true
|
||||
game_mode: true
|
||||
statistics: true
|
||||
persistent_data_container: true
|
||||
location: false
|
@ -0,0 +1,20 @@
|
||||
# Create the players table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS `%players_table%`
|
||||
(
|
||||
`uuid` char(36) NOT NULL UNIQUE,
|
||||
`username` varchar(16) NOT NULL,
|
||||
|
||||
PRIMARY KEY (`uuid`)
|
||||
);
|
||||
|
||||
# Create the player data table if it does not exist
|
||||
CREATE TABLE IF NOT EXISTS `%data_table%`
|
||||
(
|
||||
`version_uuid` char(36) NOT NULL,
|
||||
`player_uuid` char(36) NOT NULL,
|
||||
`timestamp` datetime NOT NULL,
|
||||
`data` json NOT NULL,
|
||||
|
||||
PRIMARY KEY (`version_uuid`),
|
||||
FOREIGN KEY (`player_uuid`) REFERENCES `%players_table%` (`uuid`) ON DELETE CASCADE
|
||||
);
|
@ -1,14 +0,0 @@
|
||||
synchronisation_complete: '[Daten synchronisiert!](#00fb9a)'
|
||||
viewing_inventory_of: '[Einsicht in das Inventar von](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[Einsicht in die Endertruhe von](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Meldungsdateien wurden aktualisiert.](#00fb9a)'
|
||||
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Fehler:](#ff3300) [Dieser Spieler konnte nicht gefunden werden](#ff7e5e)'
|
||||
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf das Inventar eines Online-Spielers zugreifen](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf die Endertruhe eines Online-Spielers zugreifen](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Fehler:](#ff3300) [Du kannst nicht auf dein eigenes Inventar zugreifen!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Fehler:](#ff3300) [Du kannst nicht auf deine eigene Endertruhe zugreifen!](#ff7e5e)'
|
||||
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die %1% Konsole ausgeführt werden](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Fehler:](#ff3300) [Vorgang konnte nicht verarbeitet werden; Es sind keine Server online, auf denen HuskSync installiert ist. Bitte stelle sicher, dass HuskSync sowohl auf dem Proxy-Server als auch auf allen Servern installiert ist, zwischen denen du Daten synchronisieren möchtest.](#ff7e5e)'
|
||||
error_invalid_cluster: '[Fehler:](#ff3300) [Bitte gib die ID eines gültigen Clusters an.](#ff7e5e)'
|
@ -1,14 +0,0 @@
|
||||
synchronisation_complete: '[Datos sincronizados!](#00fb9a)'
|
||||
viewing_inventory_of: '[Viendo el inventario de](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[Viendo el Ender Chest de](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Se ha reiniciado la configuración y los archivos de los mensajes.](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Sintaxis incorrecta. Uso: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar a ese jugador](#ff7e5e)'
|
||||
error_no_permission: '[Error:](#ff3300) [No tienes permiso para ejecutar este comando](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al inventario de un jugador conectado](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al Ender Chest de un jugador conectado.](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Error:](#ff3300) [No puedes acceder a tu inventario!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [No puedes acceder a tu Ender Chest!](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Ese comando solo puede ser ejecutado desde la %1% consola](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Error:](#ff3300) [Ha ocurrido un error mientras se procesaba la acción; no hay servidores online con HusckSync instalado. Por favor, asegúrate que HuskSync está instalado tanto en el proxy como en todos los servidores entre los que quieres sincronizar datos.](#ff7e5e)'
|
||||
error_invalid_cluster: '[Error:](#ff3300) [Por favor, especifica la ID de un cluster válido.](#ff7e5e)'
|
@ -1,14 +0,0 @@
|
||||
synchronisation_complete: '[データが同期されました!](#00fb9a)'
|
||||
viewing_inventory_of: '[%1%](#00fb9a bold) [のインベントリを表示します](#00fb9a) '
|
||||
viewing_ender_chest_of: '[%1%](#00fb9a bold) [のエンダーチェストを表示します](#00fb9a) '
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのインベントリにはアクセスできません](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのエンダーチェストにはアクセスできません](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Error:](#ff3300) [自分のインベントリにはアクセスできません!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [自分のエンダーチェストにはアクセスできません!](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Error:](#ff3300) [操作の処理に失敗; HuskSyncがインストールされているサーバーがオンラインになっていません。プロキシサーバーとデータを同期させたいすべてのサーバーにHuskSyncがインストールされていることを確認してください。](#ff7e5e)'
|
||||
error_invalid_cluster: '[Error:](#ff3300) [有効なクラスターのIDを指定してください。](#ff7e5e)'
|
@ -1,14 +0,0 @@
|
||||
synchronisation_complete: '[数据同步完成](#00fb9a)'
|
||||
viewing_inventory_of: '[查看玩家背包:](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[查看玩家末影箱:](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 配置与语言文件重载完成.](#00fb9a)'
|
||||
error_invalid_syntax: '[错误:](#ff3300) [格式错误. 使用方法: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[错误:](#ff3300) [未找到目标玩家](#ff7e5e)'
|
||||
error_no_permission: '[错误:](#ff3300) [你没有权限执行此命令](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家物品栏](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家末影箱](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[错误:](#ff3300) [你不能查看和编辑自己的物品栏!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[错误:](#ff3300) [你不能查看和编辑自己的末影箱!](#ff7e5e)'
|
||||
error_console_command_only: '[错误:](#ff3300) [该命令只能由 %1% 控制台执行](#ff7e5e)'
|
||||
error_no_servers_proxied: '[错误:](#ff3300) [操作处理失败; 没有任何安装了 HuskSync 的后端服务器在线. 请确认 HuskSync 已在 BungeeCord/Velocity 等代理服务器和所有你希望互相同步数据的后端服务器间安装.](#ff7e5e)'
|
||||
error_invalid_cluster: '[错误:](#ff3300) [请指定一个有效的集群(cluster) ID.](#ff7e5e)'
|
@ -1,14 +0,0 @@
|
||||
synchronisation_complete: '[資料已同步!!](#00fb9a)'
|
||||
viewing_inventory_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的背包](#00fb9a)'
|
||||
viewing_ender_chest_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的終界箱](#00fb9a)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的背包](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的終界箱](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[錯誤:](#ff3300) [您無法查看自己的背包!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[錯誤:](#ff3300) [你無法查看自己的終界箱!](#ff7e5e)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能通過 %1% 控制台運行](#ff7e5e)'
|
||||
error_no_servers_proxied: '[錯誤:](#ff3300) [處理操作失敗: 沒有安裝 HuskSync 的伺服器在線。 請確保在 Proxy 伺服器和您希望在其他同步數據的所有伺服器上都安裝了 HuskSync。](#ff7e5e)'
|
||||
error_invalid_cluster: '[錯誤:](#ff3300) [請提供有效的 Cluster ID](#ff7e5e)'
|
@ -1,28 +0,0 @@
|
||||
language: 'en-gb'
|
||||
redis_settings:
|
||||
host: 'localhost'
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
data_storage_settings:
|
||||
database_type: 'sqlite'
|
||||
mysql_settings:
|
||||
host: 'localhost'
|
||||
port: 3306
|
||||
database: 'HuskSync'
|
||||
username: 'root'
|
||||
password: 'pa55w0rd'
|
||||
params: '?autoReconnect=true&useSSL=false'
|
||||
hikari_pool_settings:
|
||||
maximum_pool_size: 10
|
||||
minimum_idle: 10
|
||||
maximum_lifetime: 1800000
|
||||
keepalive_time: 0
|
||||
connection_timeout: 5000
|
||||
bounce_back_synchronization: true
|
||||
clusters:
|
||||
main:
|
||||
player_table: 'husksync_players'
|
||||
data_table: 'husksync_data'
|
||||
check_for_updates: true
|
||||
config_file_version: 1.2
|
@ -1,3 +1,10 @@
|
||||
org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
|
||||
org.gradle.daemon=true
|
||||
javaVersion=16
|
||||
plugin_version=1.4
|
||||
plugin_archive=husksync
|
||||
|
||||
plugin_version=1.5
|
||||
plugin_archive=husksync
|
||||
|
||||
jedis_version=4.2.3
|
||||
sqlite_driver_version=3.36.0.3
|
@ -1,220 +0,0 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.velocitypowered.api.command.CommandManager;
|
||||
import com.velocitypowered.api.command.CommandMeta;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
|
||||
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
|
||||
import com.velocitypowered.api.plugin.Plugin;
|
||||
import com.velocitypowered.api.plugin.PluginContainer;
|
||||
import com.velocitypowered.api.plugin.annotation.DataDirectory;
|
||||
import com.velocitypowered.api.proxy.ProxyServer;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.data.DataManager;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.velocity.command.VelocityCommand;
|
||||
import net.william278.husksync.velocity.config.ConfigLoader;
|
||||
import net.william278.husksync.velocity.config.ConfigManager;
|
||||
import net.william278.husksync.velocity.listener.VelocityEventListener;
|
||||
import net.william278.husksync.velocity.listener.VelocityRedisListener;
|
||||
import net.william278.husksync.velocity.util.VelocityLogger;
|
||||
import net.william278.husksync.velocity.util.VelocityUpdateChecker;
|
||||
import net.byteflux.libby.Library;
|
||||
import net.byteflux.libby.VelocityLibraryManager;
|
||||
import org.bstats.velocity.Metrics;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashSet;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@Plugin(id = "husksync")
|
||||
public class HuskSyncVelocity {
|
||||
|
||||
// Plugin version
|
||||
public static String VERSION = null;
|
||||
|
||||
// Velocity bStats ID (different from Bukkit and BungeeCord)
|
||||
private static final int METRICS_ID = 13489;
|
||||
private final Metrics.Factory metricsFactory;
|
||||
|
||||
private static HuskSyncVelocity instance;
|
||||
|
||||
public static HuskSyncVelocity getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// Whether the plugin is ready to accept redis messages
|
||||
public static boolean readyForRedis = false;
|
||||
|
||||
// Whether the plugin is in the process of disabling and should skip responding to handshake confirmations
|
||||
public static boolean isDisabling = false;
|
||||
|
||||
/**
|
||||
* Set of all the {@link Server}s that have completed the synchronisation handshake with HuskSync on the proxy
|
||||
*/
|
||||
public static HashSet<Server> synchronisedServers;
|
||||
|
||||
public static DataManager dataManager;
|
||||
|
||||
public static VelocityRedisListener redisListener;
|
||||
|
||||
public static MPDBMigrator mpdbMigrator;
|
||||
|
||||
private final Logger logger;
|
||||
private final ProxyServer server;
|
||||
private final Path dataDirectory;
|
||||
|
||||
// Get the data folder
|
||||
public File getDataFolder() {
|
||||
return dataDirectory.toFile();
|
||||
}
|
||||
|
||||
// Get the proxy server
|
||||
public ProxyServer getProxyServer() {
|
||||
return server;
|
||||
}
|
||||
|
||||
// Velocity logger handling
|
||||
private VelocityLogger velocityLogger;
|
||||
|
||||
public VelocityLogger getVelocityLogger() {
|
||||
return velocityLogger;
|
||||
}
|
||||
|
||||
@Inject
|
||||
public HuskSyncVelocity(ProxyServer server, Logger logger, @DataDirectory Path dataDirectory, Metrics.Factory metricsFactory, PluginContainer pluginContainer) {
|
||||
this.server = server;
|
||||
this.logger = logger;
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.metricsFactory = metricsFactory;
|
||||
|
||||
pluginContainer.getDescription().getVersion().ifPresent(s -> VERSION = s);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onProxyInitialization(ProxyInitializeEvent event) {
|
||||
// Set instance
|
||||
instance = this;
|
||||
|
||||
// Load dependencies
|
||||
fetchDependencies();
|
||||
|
||||
// Setup logger
|
||||
velocityLogger = new VelocityLogger(logger);
|
||||
|
||||
// Prepare synchronised servers tracker
|
||||
synchronisedServers = new HashSet<>();
|
||||
|
||||
// Load config
|
||||
ConfigManager.loadConfig();
|
||||
|
||||
// Load settings from config
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
// Load messages
|
||||
ConfigManager.loadMessages();
|
||||
|
||||
// Load locales from messages
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Do update checker
|
||||
if (Settings.automaticUpdateChecks) {
|
||||
new VelocityUpdateChecker(VERSION).logToConsole();
|
||||
}
|
||||
|
||||
// Setup data manager
|
||||
dataManager = new DataManager(getVelocityLogger(), getDataFolder());
|
||||
|
||||
// Ensure the data manager initialized correctly
|
||||
if (dataManager.hasFailedInitialization) {
|
||||
getVelocityLogger().severe("Failed to initialize the HuskSync database(s).\n" +
|
||||
"HuskSync will now abort loading itself (Velocity) v" + VERSION);
|
||||
}
|
||||
|
||||
// Setup player data cache
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
dataManager.playerDataCache.put(cluster, new DataManager.PlayerDataCache());
|
||||
}
|
||||
|
||||
// Initialize the redis listener
|
||||
redisListener = new VelocityRedisListener();
|
||||
|
||||
// Register listener
|
||||
server.getEventManager().register(this, new VelocityEventListener());
|
||||
|
||||
// Register command
|
||||
CommandManager commandManager = getProxyServer().getCommandManager();
|
||||
CommandMeta meta = commandManager.metaBuilder("husksync")
|
||||
.aliases("hs")
|
||||
.build();
|
||||
commandManager.register(meta, new VelocityCommand());
|
||||
|
||||
// Prepare the migrator for use if needed
|
||||
mpdbMigrator = new MPDBMigrator(getVelocityLogger());
|
||||
|
||||
// Initialize bStats metrics
|
||||
try {
|
||||
metricsFactory.make(this, METRICS_ID);
|
||||
} catch (Exception e) {
|
||||
getVelocityLogger().info("Skipped metrics initialization");
|
||||
}
|
||||
|
||||
// Log to console
|
||||
getVelocityLogger().info("Enabled HuskSync (Velocity) v" + VERSION);
|
||||
|
||||
// Mark as ready for redis message processing
|
||||
readyForRedis = true;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onProxyShutdown(ProxyShutdownEvent event) {
|
||||
// Plugin shutdown logic
|
||||
isDisabling = true;
|
||||
|
||||
// Send terminating handshake message
|
||||
for (Server server : synchronisedServers) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.TERMINATE_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, server.clusterId()),
|
||||
server.serverUUID().toString(),
|
||||
"Velocity").send();
|
||||
} catch (IOException e) {
|
||||
getVelocityLogger().log(Level.SEVERE, "Failed to serialize Redis message for handshake termination", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Close database connections
|
||||
dataManager.closeDatabases();
|
||||
|
||||
// Log to console
|
||||
getVelocityLogger().info("Disabled HuskSync (Velocity) v" + VERSION);
|
||||
}
|
||||
|
||||
// Load dependencies
|
||||
private void fetchDependencies() {
|
||||
VelocityLibraryManager<HuskSyncVelocity> manager = new VelocityLibraryManager<>(logger, dataDirectory, getProxyServer().getPluginManager(), getInstance(), "lib");
|
||||
|
||||
Library mySqlLib = Library.builder()
|
||||
.groupId("mysql")
|
||||
.artifactId("mysql-connector-java")
|
||||
.version("8.0.29")
|
||||
.build();
|
||||
|
||||
Library sqLiteLib = Library.builder()
|
||||
.groupId("org.xerial")
|
||||
.artifactId("sqlite-jdbc")
|
||||
.version("3.36.0.3")
|
||||
.build();
|
||||
|
||||
manager.addMavenCentral();
|
||||
manager.loadLibrary(mySqlLib);
|
||||
manager.loadLibrary(sqLiteLib);
|
||||
}
|
||||
}
|
@ -1,423 +0,0 @@
|
||||
package net.william278.husksync.velocity.command;
|
||||
|
||||
import com.velocitypowered.api.command.CommandSource;
|
||||
import com.velocitypowered.api.command.SimpleCommand;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSyncVelocity;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.proxy.command.HuskSyncCommand;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import net.william278.husksync.velocity.util.VelocityUpdateChecker;
|
||||
import net.william278.husksync.velocity.config.ConfigLoader;
|
||||
import net.william278.husksync.velocity.config.ConfigManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class VelocityCommand implements SimpleCommand, HuskSyncCommand {
|
||||
|
||||
private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
|
||||
|
||||
@Override
|
||||
public void execute(Invocation invocation) {
|
||||
final String[] args = invocation.arguments();
|
||||
final CommandSource sender = invocation.source();
|
||||
if (sender instanceof Player player) {
|
||||
if (HuskSyncVelocity.synchronisedServers.size() == 0) {
|
||||
player.sendMessage(new MineDown(MessageManager.getMessage("error_no_servers_proxied")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (args.length >= 1) {
|
||||
switch (args[0].toLowerCase(Locale.ROOT)) {
|
||||
case "about", "info" -> sendAboutInformation(player);
|
||||
case "update" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown("[Checking for HuskSync updates...](gray)").toComponent());
|
||||
plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
// Check Bukkit servers needing updates
|
||||
int updatesNeeded = 0;
|
||||
String bukkitBrand = "Spigot";
|
||||
String bukkitVersion = "1.0";
|
||||
for (Server server : HuskSyncVelocity.synchronisedServers) {
|
||||
VelocityUpdateChecker updateChecker = new VelocityUpdateChecker(server.huskSyncVersion());
|
||||
if (!updateChecker.isUpToDate()) {
|
||||
updatesNeeded++;
|
||||
bukkitBrand = server.serverBrand();
|
||||
bukkitVersion = server.huskSyncVersion();
|
||||
}
|
||||
}
|
||||
|
||||
// Check Velocity servers needing updates and send message
|
||||
VelocityUpdateChecker proxyUpdateChecker = new VelocityUpdateChecker(HuskSyncVelocity.VERSION);
|
||||
if (proxyUpdateChecker.isUpToDate() && updatesNeeded == 0) {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running Version " + proxyUpdateChecker.getLatestVersion() + "](#00fb9a)").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| Your server(s) are not up-to-date:](#00fb9a)").toComponent());
|
||||
if (!proxyUpdateChecker.isUpToDate()) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on the Velocity proxy is outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + proxyUpdateChecker.getCurrentVersion() + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
if (updatesNeeded > 0) {
|
||||
sender.sendMessage(new MineDown("[•](white) [HuskSync on " + updatesNeeded + " connected " + bukkitBrand + " server(s) are outdated (Latest: " + proxyUpdateChecker.getLatestVersion() + ", Running: " + bukkitVersion + ")](#00fb9a)").toComponent());
|
||||
}
|
||||
sender.sendMessage(new MineDown("[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husktowns.92672/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husktowns.1056/updates)").toComponent());
|
||||
}
|
||||
}).schedule();
|
||||
}
|
||||
case "invsee", "openinv", "inventory" -> {
|
||||
if (!player.hasPermission("husksync.command.inventory")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openInventory(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync invsee <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "echest", "enderchest" -> {
|
||||
if (!player.hasPermission("husksync.command.ender_chest")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
String clusterId;
|
||||
if (Settings.clusters.size() > 1) {
|
||||
if (args.length == 3) {
|
||||
clusterId = args[2];
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
clusterId = "main";
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
clusterId = cluster.clusterId();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (args.length == 2 || args.length == 3) {
|
||||
String playerName = args[1];
|
||||
openEnderChest(player, playerName, clusterId);
|
||||
} else {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax")
|
||||
.replaceAll("%1%", "/husksync echest <player>")).toComponent());
|
||||
}
|
||||
}
|
||||
case "migrate" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_console_command_only")
|
||||
.replaceAll("%1%", "Velocity")).toComponent());
|
||||
}
|
||||
case "status" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
int playerDataSize = 0;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
playerDataSize += HuskSyncVelocity.dataManager.playerDataCache.get(cluster).playerData.size();
|
||||
}
|
||||
sender.sendMessage(new MineDown(MessageManager.PLUGIN_STATUS.toString()
|
||||
.replaceAll("%1%", String.valueOf(HuskSyncVelocity.synchronisedServers.size()))
|
||||
.replaceAll("%2%", String.valueOf(playerDataSize))).toComponent());
|
||||
}
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission("husksync.command.admin")) {
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("error_no_permission")).toComponent());
|
||||
return;
|
||||
}
|
||||
ConfigManager.loadConfig();
|
||||
ConfigLoader.loadSettings(Objects.requireNonNull(ConfigManager.getConfig()));
|
||||
|
||||
ConfigManager.loadMessages();
|
||||
ConfigLoader.loadMessageStrings(Objects.requireNonNull(ConfigManager.getMessages()));
|
||||
|
||||
// Send reload request to all bukkit servers
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.RELOAD_CONFIG,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
"reload")
|
||||
.send();
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize reload notification message data");
|
||||
}
|
||||
|
||||
sender.sendMessage(new MineDown(MessageManager.getMessage("reload_complete")).toComponent());
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_syntax").replaceAll("%1%",
|
||||
"/husksync <about/status/invsee/echest>")).toComponent());
|
||||
}
|
||||
} else {
|
||||
sendAboutInformation(player);
|
||||
}
|
||||
} else {
|
||||
// Database migration wizard
|
||||
if (args.length >= 1) {
|
||||
if (args[0].equalsIgnoreCase("migrate")) {
|
||||
MPDBMigrator migrator = HuskSyncVelocity.mpdbMigrator;
|
||||
if (args.length == 1) {
|
||||
sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
This will migrate data from the MySQLPlayerDataBridge
|
||||
plugin to HuskSync.
|
||||
|
||||
Data that will be migrated:
|
||||
- Inventories
|
||||
- Ender Chests
|
||||
- Experience points
|
||||
|
||||
Other non-vital data, such as current health, hunger
|
||||
& potion effects will not be migrated to ensure that
|
||||
migration does not take an excessive amount of time.
|
||||
|
||||
To do this, you need to have MySqlPlayerDataBridge
|
||||
and HuskSync installed on one Spigot server as well
|
||||
as HuskSync installed on the proxy (which you have)
|
||||
|
||||
>To proceed, type: husksync migrate setup""").toComponent());
|
||||
} else {
|
||||
switch (args[1].toLowerCase()) {
|
||||
case "setup" -> sender.sendMessage(new MineDown(
|
||||
"""
|
||||
=== MySQLPlayerDataBridge Migration Wizard ==========
|
||||
The following database settings will be used.
|
||||
Please make sure they match the correct settings to
|
||||
access your MySQLPlayerDataBridge Data
|
||||
|
||||
sourceHost: %1%
|
||||
sourcePort: %2%
|
||||
sourceDatabase: %3%
|
||||
sourceUsername: %4%
|
||||
sourcePassword: %5%
|
||||
|
||||
sourceInventoryTableName: %6%
|
||||
sourceEnderChestTableName: %7%
|
||||
sourceExperienceTableName: %8%
|
||||
|
||||
targetCluster: %9%
|
||||
|
||||
To change a setting, type:
|
||||
husksync migrate setting <settingName> <value>
|
||||
|
||||
Please ensure no players are logged in to the network
|
||||
and that at least one Spigot server is online with
|
||||
both HuskSync AND MySqlPlayerDataBridge installed AND
|
||||
that the server has been configured with the correct
|
||||
Redis credentials.
|
||||
|
||||
Warning: Data will be saved to your configured data
|
||||
source, which is currently a %10% database.
|
||||
Please make sure you are happy with this, or stop
|
||||
the proxy server and edit this in config.yml
|
||||
|
||||
Warning: Migration will overwrite any current data
|
||||
saved by HuskSync. It will not, however, delete any
|
||||
data from the source MySQLPlayerDataBridge database.
|
||||
|
||||
>When done, type: husksync migrate start"""
|
||||
.replaceAll("%1%", migrator.migrationSettings.sourceHost)
|
||||
.replaceAll("%2%", String.valueOf(migrator.migrationSettings.sourcePort))
|
||||
.replaceAll("%3%", migrator.migrationSettings.sourceDatabase)
|
||||
.replaceAll("%4%", migrator.migrationSettings.sourceUsername)
|
||||
.replaceAll("%5%", migrator.migrationSettings.sourcePassword)
|
||||
.replaceAll("%6%", migrator.migrationSettings.inventoryDataTable)
|
||||
.replaceAll("%7%", migrator.migrationSettings.enderChestDataTable)
|
||||
.replaceAll("%8%", migrator.migrationSettings.expDataTable)
|
||||
.replaceAll("%9%", migrator.migrationSettings.targetCluster)
|
||||
.replaceAll("%10%", Settings.dataStorageType.toString())
|
||||
).toComponent());
|
||||
case "setting" -> {
|
||||
if (args.length == 4) {
|
||||
String value = args[3];
|
||||
switch (args[2]) {
|
||||
case "sourceHost", "host" -> migrator.migrationSettings.sourceHost = value;
|
||||
case "sourcePort", "port" -> {
|
||||
try {
|
||||
migrator.migrationSettings.sourcePort = Integer.parseInt(value);
|
||||
} catch (NumberFormatException e) {
|
||||
sender.sendMessage(new MineDown("Error: Invalid value; port must be a number").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
case "sourceDatabase", "database" -> migrator.migrationSettings.sourceDatabase = value;
|
||||
case "sourceUsername", "username" -> migrator.migrationSettings.sourceUsername = value;
|
||||
case "sourcePassword", "password" -> migrator.migrationSettings.sourcePassword = value;
|
||||
case "sourceInventoryTableName", "inventoryTableName", "inventoryTable" -> migrator.migrationSettings.inventoryDataTable = value;
|
||||
case "sourceEnderChestTableName", "enderChestTableName", "enderChestTable" -> migrator.migrationSettings.enderChestDataTable = value;
|
||||
case "sourceExperienceTableName", "experienceTableName", "experienceTable" -> migrator.migrationSettings.expDataTable = value;
|
||||
case "targetCluster", "cluster" -> migrator.migrationSettings.targetCluster = value;
|
||||
default -> {
|
||||
sender.sendMessage(new MineDown("Error: Invalid setting; please use \"husksync migrate setup\" to view a list").toComponent());
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Successfully updated setting: \"" + args[2] + "\" --> \"" + value + "\"").toComponent());
|
||||
} else {
|
||||
sender.sendMessage(new MineDown("Error: Invalid usage. Syntax: husksync migrate setting <settingName> <value>").toComponent());
|
||||
}
|
||||
}
|
||||
case "start" -> {
|
||||
sender.sendMessage(new MineDown("Starting MySQLPlayerDataBridge migration!...").toComponent());
|
||||
|
||||
// If the migrator is ready, execute the migration asynchronously
|
||||
if (HuskSyncVelocity.mpdbMigrator.readyToMigrate(plugin.getProxyServer().getPlayerCount(),
|
||||
HuskSyncVelocity.synchronisedServers)) {
|
||||
plugin.getProxyServer().getScheduler().buildTask(plugin, () ->
|
||||
HuskSyncVelocity.mpdbMigrator.executeMigrationOperations(HuskSyncVelocity.dataManager,
|
||||
HuskSyncVelocity.synchronisedServers, HuskSyncVelocity.redisListener)).schedule();
|
||||
}
|
||||
}
|
||||
default -> sender.sendMessage(new MineDown("Error: Invalid argument for migration. Use \"husksync migrate\" to start the process").toComponent());
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
sender.sendMessage(new MineDown("Error: Invalid syntax. Usage: husksync migrate <args>").toComponent());
|
||||
}
|
||||
}
|
||||
|
||||
// View the inventory of a player specified by their name
|
||||
private void openInventory(Player viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getUsername().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_inventory")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_inventory_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncVelocity.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_INVENTORY,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_inventory_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
}).schedule();
|
||||
}
|
||||
|
||||
// View the ender chest of a player specified by their name
|
||||
public void openEnderChest(Player viewer, String targetPlayerName, String clusterId) {
|
||||
if (viewer.getUsername().equalsIgnoreCase(targetPlayerName)) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_own_ender_chest")).toComponent());
|
||||
return;
|
||||
}
|
||||
if (plugin.getProxyServer().getPlayer(targetPlayerName).isPresent()) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_cannot_view_ender_chest_online")).toComponent());
|
||||
return;
|
||||
}
|
||||
plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (!cluster.clusterId().equals(clusterId)) continue;
|
||||
PlayerData playerData = HuskSyncVelocity.dataManager.getPlayerDataByName(targetPlayerName, cluster.clusterId());
|
||||
if (playerData == null) {
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_player")).toComponent());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.OPEN_ENDER_CHEST,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, viewer.getUniqueId(), null),
|
||||
targetPlayerName, RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("viewing_ender_chest_of").replaceAll("%1%",
|
||||
targetPlayerName)).toComponent());
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize inventory-see player data", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
viewer.sendMessage(new MineDown(MessageManager.getMessage("error_invalid_cluster")).toComponent());
|
||||
}).schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send information about the plugin
|
||||
*
|
||||
* @param player The player to send it to
|
||||
*/
|
||||
private void sendAboutInformation(Player player) {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.SEND_PLUGIN_INFORMATION,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, player.getUniqueId(), null),
|
||||
"Velocity", HuskSyncVelocity.VERSION).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.WARNING, "Failed to serialize plugin information to send", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> suggest(Invocation invocation) {
|
||||
final CommandSource sender = invocation.source();
|
||||
final String[] args = invocation.arguments();
|
||||
|
||||
if (sender instanceof Player player) {
|
||||
if (args.length == 1) {
|
||||
final ArrayList<String> subCommands = new ArrayList<>();
|
||||
for (SubCommand subCommand : SUB_COMMANDS) {
|
||||
if (subCommand.permission() != null) {
|
||||
if (!player.hasPermission(subCommand.permission())) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
subCommands.add(subCommand.command());
|
||||
}
|
||||
// Return list of subcommands
|
||||
if (args[0].length() == 0) {
|
||||
return subCommands;
|
||||
}
|
||||
|
||||
// Automatically filter the sub commands' order in tab completion by what the player has typed
|
||||
return subCommands.stream().filter(val -> val.startsWith(args[0]))
|
||||
.sorted().collect(Collectors.toList());
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package net.william278.husksync.velocity.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncVelocity;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
import ninja.leaping.configurate.ConfigurationNode;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ConfigLoader {
|
||||
|
||||
private static ConfigurationNode copyDefaults(ConfigurationNode configRoot) {
|
||||
// Get the config version and update if needed
|
||||
String configVersion = getConfigString(configRoot, "1.0", "config_file_version");
|
||||
if (configVersion.contains("-dev")) {
|
||||
configVersion = configVersion.replaceAll("-dev", "");
|
||||
}
|
||||
if (!configVersion.equals(HuskSyncVelocity.VERSION)) {
|
||||
if (configVersion.equalsIgnoreCase("1.0")) {
|
||||
configRoot.getNode("check_for_updates").setValue(true);
|
||||
}
|
||||
if (configVersion.equalsIgnoreCase("1.0") || configVersion.equalsIgnoreCase("1.0.1") || configVersion.equalsIgnoreCase("1.0.2") || configVersion.equalsIgnoreCase("1.0.3")) {
|
||||
configRoot.getNode("clusters", "main", "player_table").setValue("husksync_players");
|
||||
configRoot.getNode("clusters", "main", "data_table").setValue("husksync_data");
|
||||
}
|
||||
configRoot.getNode("config_file_version").setValue(HuskSyncVelocity.VERSION);
|
||||
}
|
||||
// Save the config back
|
||||
ConfigManager.saveConfig(configRoot);
|
||||
return configRoot;
|
||||
}
|
||||
|
||||
private static String getConfigString(ConfigurationNode rootNode, String defaultValue, String... nodePath) {
|
||||
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getString() : defaultValue;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static boolean getConfigBoolean(ConfigurationNode rootNode, boolean defaultValue, String... nodePath) {
|
||||
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getBoolean() : defaultValue;
|
||||
}
|
||||
|
||||
private static int getConfigInt(ConfigurationNode rootNode, int defaultValue, String... nodePath) {
|
||||
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getInt() : defaultValue;
|
||||
}
|
||||
|
||||
private static long getConfigLong(ConfigurationNode rootNode, long defaultValue, String... nodePath) {
|
||||
return !rootNode.getNode((Object[]) nodePath).isVirtual() ? rootNode.getNode((Object[]) nodePath).getLong() : defaultValue;
|
||||
}
|
||||
|
||||
public static void loadSettings(ConfigurationNode loadedConfig) throws IllegalArgumentException {
|
||||
ConfigurationNode config = copyDefaults(loadedConfig);
|
||||
|
||||
Settings.language = getConfigString(config, "en-gb", "language");
|
||||
|
||||
Settings.serverType = Settings.ServerType.PROXY;
|
||||
Settings.automaticUpdateChecks = getConfigBoolean(config, true, "check_for_updates");
|
||||
Settings.redisHost = getConfigString(config, "localhost", "redis_settings", "host");
|
||||
Settings.redisPort = getConfigInt(config, 6379, "redis_settings", "port");
|
||||
Settings.redisPassword = getConfigString(config, "", "redis_settings", "password");
|
||||
Settings.redisSSL = getConfigBoolean(config, false, "redis_settings", "use_ssl");
|
||||
|
||||
Settings.dataStorageType = Settings.DataStorageType.valueOf(getConfigString(config, "sqlite", "data_storage_settings", "database_type").toUpperCase());
|
||||
if (Settings.dataStorageType == Settings.DataStorageType.MYSQL) {
|
||||
Settings.mySQLHost = getConfigString(config, "localhost", "data_storage_settings", "mysql_settings", "host");
|
||||
Settings.mySQLPort = getConfigInt(config, 3306, "data_storage_settings", "mysql_settings", "port");
|
||||
Settings.mySQLDatabase = getConfigString(config, "HuskSync", "data_storage_settings", "mysql_settings", "database");
|
||||
Settings.mySQLUsername = getConfigString(config, "root", "data_storage_settings", "mysql_settings", "username");
|
||||
Settings.mySQLPassword = getConfigString(config, "pa55w0rd", "data_storage_settings", "mysql_settings", "password");
|
||||
Settings.mySQLParams = getConfigString(config, "?autoReconnect=true&useSSL=false", "data_storage_settings", "mysql_settings", "params");
|
||||
}
|
||||
|
||||
Settings.hikariMaximumPoolSize = getConfigInt(config, 10, "data_storage_settings", "hikari_pool_settings", "maximum_pool_size");
|
||||
Settings.hikariMinimumIdle = getConfigInt(config, 10, "data_storage_settings", "hikari_pool_settings", "minimum_idle");
|
||||
Settings.hikariMaximumLifetime = getConfigLong(config, 1800000, "data_storage_settings", "hikari_pool_settings", "maximum_lifetime");
|
||||
Settings.hikariKeepAliveTime = getConfigLong(config, 0, "data_storage_settings", "hikari_pool_settings", "keepalive_time");
|
||||
Settings.hikariConnectionTimeOut = getConfigLong(config, 5000, "data_storage_settings", "hikari_pool_settings", "connection_timeout");
|
||||
|
||||
Settings.bounceBackSynchronisation = getConfigBoolean(config, true,"bounce_back_synchronization");
|
||||
|
||||
// Read cluster data
|
||||
ConfigurationNode clusterSection = config.getNode("clusters");
|
||||
final String settingDatabaseName = Settings.mySQLDatabase != null ? Settings.mySQLDatabase : "HuskSync";
|
||||
for (ConfigurationNode cluster : clusterSection.getChildrenMap().values()) {
|
||||
final String clusterId = (String) cluster.getKey();
|
||||
final String playerTableName = getConfigString(config, "husksync_players", "clusters", clusterId, "player_table");
|
||||
final String dataTableName = getConfigString(config, "husksync_data", "clusters", clusterId, "data_table");
|
||||
final String databaseName = getConfigString(config, settingDatabaseName, "clusters", clusterId, "database");
|
||||
Settings.clusters.add(new Settings.SynchronisationCluster(clusterId, databaseName, playerTableName, dataTableName));
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessageStrings(ConfigurationNode config) {
|
||||
final HashMap<String, String> messages = new HashMap<>();
|
||||
for (ConfigurationNode message : config.getChildrenMap().values()) {
|
||||
final String messageId = (String) message.getKey();
|
||||
messages.put(messageId, getConfigString(config, "", messageId));
|
||||
}
|
||||
MessageManager.setMessages(messages);
|
||||
}
|
||||
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
package net.william278.husksync.velocity.config;
|
||||
|
||||
import net.william278.husksync.HuskSyncVelocity;
|
||||
import net.william278.husksync.Settings;
|
||||
import ninja.leaping.configurate.ConfigurationNode;
|
||||
import ninja.leaping.configurate.yaml.YAMLConfigurationLoader;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ConfigManager {
|
||||
|
||||
private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
|
||||
|
||||
public static void loadConfig() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getVelocityLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
if (!configFile.exists()) {
|
||||
Files.copy(Objects.requireNonNull(HuskSyncVelocity.class.getClassLoader().getResourceAsStream("proxy-config.yml")), configFile.toPath());
|
||||
plugin.getVelocityLogger().info("Created HuskSync config file");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getVelocityLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void saveConfig(ConfigurationNode rootNode) {
|
||||
try {
|
||||
getConfigLoader().save(rootNode);
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.CONFIG, "An exception occurred loading the configuration file", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadMessages() {
|
||||
try {
|
||||
if (!plugin.getDataFolder().exists()) {
|
||||
if (plugin.getDataFolder().mkdir()) {
|
||||
plugin.getVelocityLogger().info("Created HuskSync data folder");
|
||||
}
|
||||
}
|
||||
File messagesFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
if (!messagesFile.exists()) {
|
||||
Files.copy(Objects.requireNonNull(HuskSyncVelocity.class.getClassLoader().getResourceAsStream("languages/" + Settings.language + ".yml")),
|
||||
messagesFile.toPath());
|
||||
plugin.getVelocityLogger().info("Created HuskSync messages file");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.CONFIG, "An exception occurred loading the messages file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static YAMLConfigurationLoader getConfigLoader() {
|
||||
File configFile = new File(plugin.getDataFolder(), "config.yml");
|
||||
return YAMLConfigurationLoader.builder()
|
||||
.setPath(configFile.toPath())
|
||||
.setFlowStyle(DumperOptions.FlowStyle.BLOCK)
|
||||
.setIndent(2)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static ConfigurationNode getConfig() {
|
||||
try {
|
||||
return getConfigLoader().load();
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.CONFIG, "An IOException has occurred loading the plugin config.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static ConfigurationNode getMessages() {
|
||||
try {
|
||||
File configFile = new File(plugin.getDataFolder(), "messages_" + Settings.language + ".yml");
|
||||
return YAMLConfigurationLoader.builder()
|
||||
.setPath(configFile.toPath())
|
||||
.setFlowStyle(DumperOptions.FlowStyle.BLOCK)
|
||||
.setIndent(2)
|
||||
.build()
|
||||
.load();
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.CONFIG, "An IOException has occurred loading the plugin messages.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
package net.william278.husksync.velocity.listener;
|
||||
|
||||
import com.velocitypowered.api.event.PostOrder;
|
||||
import com.velocitypowered.api.event.Subscribe;
|
||||
import com.velocitypowered.api.event.connection.PostLoginEvent;
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import net.william278.husksync.HuskSyncVelocity;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class VelocityEventListener {
|
||||
|
||||
private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
|
||||
|
||||
@Subscribe(order = PostOrder.FIRST)
|
||||
public void onPostLogin(PostLoginEvent event) {
|
||||
final Player player = event.getPlayer();
|
||||
plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
// Ensure the player has data on SQL and that it is up-to-date
|
||||
HuskSyncVelocity.dataManager.ensurePlayerExists(player.getUniqueId(), player.getUsername());
|
||||
|
||||
// Get the player's data from SQL
|
||||
final Map<Settings.SynchronisationCluster, PlayerData> data = HuskSyncVelocity.dataManager.getPlayerData(player.getUniqueId());
|
||||
|
||||
// Update the player's data from SQL onto the cache
|
||||
assert data != null;
|
||||
for (Settings.SynchronisationCluster cluster : data.keySet()) {
|
||||
HuskSyncVelocity.dataManager.playerDataCache.get(cluster).updatePlayer(data.get(cluster));
|
||||
}
|
||||
|
||||
// Send a message asking the bukkit to request data on join
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, null),
|
||||
RedisMessage.RequestOnJoinUpdateType.ADD_REQUESTER.toString(), player.getUniqueId().toString()).send();
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.SEVERE, "Failed to serialize request data on join message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).schedule();
|
||||
}
|
||||
}
|
@ -1,231 +0,0 @@
|
||||
package net.william278.husksync.velocity.listener;
|
||||
|
||||
import com.velocitypowered.api.proxy.Player;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSyncVelocity;
|
||||
import net.william278.husksync.PlayerData;
|
||||
import net.william278.husksync.Server;
|
||||
import net.william278.husksync.Settings;
|
||||
import net.william278.husksync.migrator.MPDBMigrator;
|
||||
import net.william278.husksync.redis.RedisListener;
|
||||
import net.william278.husksync.redis.RedisMessage;
|
||||
import net.william278.husksync.util.MessageManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class VelocityRedisListener extends RedisListener {
|
||||
|
||||
private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
|
||||
|
||||
// Initialize the listener on the bungee
|
||||
public VelocityRedisListener() {
|
||||
super();
|
||||
listen();
|
||||
}
|
||||
|
||||
private PlayerData getPlayerCachedData(UUID uuid, String clusterId) {
|
||||
PlayerData data = null;
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(clusterId)) {
|
||||
// Get the player data from the cache
|
||||
PlayerData cachedData = HuskSyncVelocity.dataManager.playerDataCache.get(cluster).getPlayer(uuid);
|
||||
if (cachedData != null) {
|
||||
return cachedData;
|
||||
}
|
||||
|
||||
data = Objects.requireNonNull(HuskSyncVelocity.dataManager.getPlayerData(uuid)).get(cluster); // Get their player data from MySQL
|
||||
HuskSyncVelocity.dataManager.playerDataCache.get(cluster).updatePlayer(data); // Update the cache
|
||||
break;
|
||||
}
|
||||
}
|
||||
return data; // Return the data
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an incoming {@link RedisMessage}
|
||||
*
|
||||
* @param message The {@link RedisMessage} to handle
|
||||
*/
|
||||
@Override
|
||||
public void handleMessage(RedisMessage message) {
|
||||
// Ignore messages destined for Bukkit servers
|
||||
if (message.getMessageTarget().targetServerType() != Settings.ServerType.PROXY) {
|
||||
return;
|
||||
}
|
||||
// Only process redis messages when ready
|
||||
if (!HuskSyncVelocity.readyForRedis) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.getMessageType()) {
|
||||
case PLAYER_DATA_REQUEST -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
// Get the UUID of the requesting player
|
||||
final UUID requestingPlayerUUID = UUID.fromString(message.getMessageData());
|
||||
try {
|
||||
// Send the reply, serializing the message data
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, requestingPlayerUUID, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.serialize(getPlayerCachedData(requestingPlayerUUID, message.getMessageTarget().targetClusterId())))
|
||||
.send();
|
||||
|
||||
// Send an update to all bukkit servers removing the player from the requester cache
|
||||
new RedisMessage(RedisMessage.MessageType.REQUEST_DATA_ON_JOIN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.RequestOnJoinUpdateType.REMOVE_REQUESTER.toString(), requestingPlayerUUID.toString())
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
Optional<Player> player = plugin.getProxyServer().getPlayer(requestingPlayerUUID);
|
||||
player.ifPresent(value -> value.sendActionBar(new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent()));
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize data when replying to a data request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}).schedule();
|
||||
case PLAYER_DATA_UPDATE -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final boolean bounceBack = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the data in the cache and SQL
|
||||
for (Settings.SynchronisationCluster cluster : Settings.clusters) {
|
||||
if (cluster.clusterId().equals(message.getMessageTarget().targetClusterId())) {
|
||||
HuskSyncVelocity.dataManager.updatePlayerData(playerData, cluster);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Reply with the player data if they are still online (switching server)
|
||||
if (Settings.bounceBackSynchronisation && bounceBack) {
|
||||
Optional<Player> updatingPlayer = plugin.getProxyServer().getPlayer(playerData.getPlayerUUID());
|
||||
updatingPlayer.ifPresent(player -> {
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.PLAYER_DATA_SET,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, playerData.getPlayerUUID(), message.getMessageTarget().targetClusterId()),
|
||||
RedisMessage.serialize(playerData))
|
||||
.send();
|
||||
|
||||
// Send synchronisation complete message
|
||||
player.sendActionBar(new MineDown(MessageManager.getMessage("synchronisation_complete")).toComponent());
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to re-serialize PlayerData when handling a player update request");
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
}).schedule();
|
||||
case CONNECTION_HANDSHAKE -> {
|
||||
// Reply to a Bukkit server's connection handshake to complete the process
|
||||
if (HuskSyncVelocity.isDisabling) return; // Return if the Proxy is disabling
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final boolean hasMySqlPlayerDataBridge = Boolean.parseBoolean(message.getMessageDataElements()[1]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[2];
|
||||
final String huskSyncVersion = message.getMessageDataElements()[3];
|
||||
try {
|
||||
new RedisMessage(RedisMessage.MessageType.CONNECTION_HANDSHAKE,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
serverUUID.toString(), "Velocity")
|
||||
.send();
|
||||
HuskSyncVelocity.synchronisedServers.add(
|
||||
new Server(serverUUID, hasMySqlPlayerDataBridge,
|
||||
huskSyncVersion, bukkitBrand, message.getMessageTarget().targetClusterId()));
|
||||
log(Level.INFO, "Completed handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
} catch (IOException e) {
|
||||
log(Level.SEVERE, "Failed to serialize handshake message data");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
case TERMINATE_HANDSHAKE -> {
|
||||
// Terminate the handshake with a Bukkit server
|
||||
final UUID serverUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final String bukkitBrand = message.getMessageDataElements()[1];
|
||||
|
||||
// Remove a server from the synchronised server list
|
||||
Server serverToRemove = null;
|
||||
for (Server server : HuskSyncVelocity.synchronisedServers) {
|
||||
if (server.serverUUID().equals(serverUUID)) {
|
||||
serverToRemove = server;
|
||||
break;
|
||||
}
|
||||
}
|
||||
HuskSyncVelocity.synchronisedServers.remove(serverToRemove);
|
||||
log(Level.INFO, "Terminated the handshake with " + bukkitBrand + " server (" + serverUUID + ")");
|
||||
}
|
||||
case DECODED_MPDB_DATA_SET -> {
|
||||
// Deserialize the PlayerData received
|
||||
PlayerData playerData;
|
||||
final String serializedPlayerData = message.getMessageDataElements()[0];
|
||||
final String playerName = message.getMessageDataElements()[1];
|
||||
try {
|
||||
playerData = (PlayerData) RedisMessage.deserialize(serializedPlayerData);
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
log(Level.SEVERE, "Failed to deserialize PlayerData when handling incoming decoded MPDB data");
|
||||
e.printStackTrace();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the MPDB migrator
|
||||
MPDBMigrator migrator = HuskSyncVelocity.mpdbMigrator;
|
||||
|
||||
// Add the incoming data to the data to be saved
|
||||
migrator.incomingPlayerData.put(playerData, playerName);
|
||||
|
||||
// Increment players migrated
|
||||
migrator.playersMigrated++;
|
||||
plugin.getVelocityLogger().log(Level.INFO, "Migrated " + migrator.playersMigrated + "/" + migrator.migratedDataSent + " players.");
|
||||
|
||||
// When all the data has been received, save it
|
||||
if (migrator.migratedDataSent == migrator.playersMigrated) {
|
||||
migrator.loadIncomingData(migrator.incomingPlayerData, HuskSyncVelocity.dataManager);
|
||||
}
|
||||
}
|
||||
case API_DATA_REQUEST -> plugin.getProxyServer().getScheduler().buildTask(plugin, () -> {
|
||||
final UUID playerUUID = UUID.fromString(message.getMessageDataElements()[0]);
|
||||
final UUID requestUUID = UUID.fromString(message.getMessageDataElements()[1]);
|
||||
|
||||
try {
|
||||
final PlayerData data = getPlayerCachedData(playerUUID, message.getMessageTarget().targetClusterId());
|
||||
|
||||
if (data == null) {
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_CANCEL,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString())
|
||||
.send();
|
||||
} else {
|
||||
// Send the reply alongside the request UUID, serializing the requested message data
|
||||
new RedisMessage(RedisMessage.MessageType.API_DATA_RETURN,
|
||||
new RedisMessage.MessageTarget(Settings.ServerType.BUKKIT, null, message.getMessageTarget().targetClusterId()),
|
||||
requestUUID.toString(),
|
||||
RedisMessage.serialize(data))
|
||||
.send();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
plugin.getVelocityLogger().log(Level.SEVERE, "Failed to serialize PlayerData requested via the API");
|
||||
}
|
||||
}).schedule();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log to console
|
||||
*
|
||||
* @param level The {@link Level} to log
|
||||
* @param message Message to log
|
||||
*/
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getVelocityLogger().log(level, message);
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
package net.william278.husksync.velocity.util;
|
||||
|
||||
import net.william278.husksync.util.Logger;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public record VelocityLogger(org.slf4j.Logger parent) implements Logger {
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message, Exception e) {
|
||||
logMessage(level, message);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
logMessage(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
logMessage(Level.INFO, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(String message) {
|
||||
logMessage(Level.SEVERE, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(String message) {
|
||||
logMessage(Level.CONFIG, message);
|
||||
}
|
||||
|
||||
// Logs the message using SLF4J
|
||||
private void logMessage(Level level, String message) {
|
||||
switch (level.intValue()) {
|
||||
case 1000 -> parent.error(message); // Severe
|
||||
case 900 -> parent.warn(message); // Warning
|
||||
case 70 -> parent.warn("[Config] " + message);
|
||||
default -> parent.info(message);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package net.william278.husksync.velocity.util;
|
||||
|
||||
import net.william278.husksync.HuskSyncVelocity;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class VelocityUpdateChecker extends UpdateChecker {
|
||||
|
||||
private static final HuskSyncVelocity plugin = HuskSyncVelocity.getInstance();
|
||||
|
||||
public VelocityUpdateChecker(String versionToCheck) {
|
||||
super(versionToCheck);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
plugin.getVelocityLogger().log(level, message);
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"id": "husksync",
|
||||
"name": "HuskSync",
|
||||
"version": "${version}",
|
||||
"description": "A modern, cross-server player data synchronization system",
|
||||
"url": "https://william278.net",
|
||||
"authors": [
|
||||
"William278"
|
||||
],
|
||||
"dependencies": [],
|
||||
"main": "net.william278.husksync.HuskSyncVelocity"
|
||||
}
|
Loading…
Reference in New Issue