mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-25 17:49:20 +00:00
Start 2.0 rewrite
Use redis key caching, remove need for proxy plugin Make platform independent to allow porting to other platforms
This commit is contained in:
38
common/src/main/java/net/william278/husksync/HuskSync.java
Normal file
38
common/src/main/java/net/william278/husksync/HuskSync.java
Normal file
@@ -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);
|
||||
|
||||
}
|
||||
139
common/src/main/java/net/william278/husksync/config/Locales.java
Normal file
139
common/src/main/java/net/william278/husksync/config/Locales.java
Normal file
@@ -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() {
|
||||
}
|
||||
}
|
||||
159
common/src/main/java/net/william278/husksync/data/UserData.java
Normal file
159
common/src/main/java/net/william278/husksync/data/UserData.java
Normal file
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package net.william278.husksync.util;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Logger interface to allow for implementation of different logger platforms used by Bungee and Velocity
|
||||
* An abstract, cross-platform representation of a logger
|
||||
*/
|
||||
public interface Logger {
|
||||
|
||||
@@ -16,4 +16,5 @@ public interface Logger {
|
||||
void severe(String message);
|
||||
|
||||
void config(String message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,4 +58,4 @@ public class VersionUtils {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user