9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-29 03:29:12 +00:00

Basic bukkit implementation

This commit is contained in:
William
2022-07-03 18:14:44 +01:00
parent 9471e0cbff
commit 38c261871a
40 changed files with 1643 additions and 285 deletions

View File

@@ -2,16 +2,16 @@ 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.database.Database;
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;
import java.util.concurrent.CompletableFuture;
public interface HuskSync {
@@ -19,8 +19,6 @@ public interface HuskSync {
@NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid);
@NotNull EventListener getEventListener();
@NotNull Database getDatabase();
@NotNull RedisManager getRedisManager();
@@ -29,10 +27,10 @@ public interface HuskSync {
@NotNull Locales getLocales();
@NotNull Logger getLogger();
@NotNull Logger getLoggingAdapter();
@NotNull String getVersion();
void reload();
CompletableFuture<Boolean> reload();
}

View File

@@ -28,7 +28,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
return;
}
final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), plugin.getLogger());
final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter());
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)" +
@@ -56,22 +56,22 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
@Override
public void onConsoleExecute(@NotNull String[] args) {
if (args.length < 1) {
plugin.getLogger().log(Level.INFO, "Console usage: /husksync <update|info|reload|migrate>");
plugin.getLoggingAdapter().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(
case "update", "version" -> new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole();
case "info", "about" -> plugin.getLoggingAdapter().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.");
plugin.getLoggingAdapter().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>");
plugin.getLoggingAdapter().log(Level.INFO, "Invalid syntax. Console usage: /husksync <update|info|reload|migrate>");
}
}

View File

@@ -118,7 +118,7 @@ public class Settings {
LANGUAGE("language", OptionType.STRING, "en-gb"),
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
CLUSTER_ID("cluster_id", OptionType.STRING, ""), //todo implement this
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),

View File

@@ -1,6 +1,7 @@
package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.Date;
import java.util.Map;
@@ -25,4 +26,8 @@ public class AdvancementData {
public AdvancementData() {
}
public AdvancementData(@NotNull String key, @NotNull Map<String, Date> awardedCriteria) {
this.key = key;
this.completedCriteria = awardedCriteria;
}
}

View File

@@ -3,19 +3,21 @@ package net.william278.husksync.data;
import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
/**
* 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
* Map of namespaced key strings to a byte array representing the persistent data
*/
@SerializedName("serialized_persistent_data_container")
public String serializedPersistentDataContainer;
@SerializedName("persistent_data_map")
public Map<String, Byte[]> persistentDataMap;
public PersistentDataContainerData(@NotNull final String serializedPersistentDataContainer) {
this.serializedPersistentDataContainer = serializedPersistentDataContainer;
public PersistentDataContainerData(@NotNull final Map<String, Byte[]> persistentDataMap) {
this.persistentDataMap = persistentDataMap;
}
public PersistentDataContainerData() {

View File

@@ -4,6 +4,7 @@ import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
/**
* Stores information about a player's statistics
@@ -14,30 +15,30 @@ public class StatisticsData {
* Map of untyped statistic names to their values
*/
@SerializedName("untyped_statistics")
public HashMap<String, Integer> untypedStatistic;
public Map<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;
public Map<String, Map<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;
public Map<String, Map<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 Map<String, Map<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) {
public StatisticsData(@NotNull Map<String, Integer> untypedStatistic,
@NotNull Map<String, Map<String, Integer>> blockStatistics,
@NotNull Map<String, Map<String, Integer>> itemStatistics,
@NotNull Map<String, Map<String, Integer>> entityStatistics) {
this.untypedStatistic = untypedStatistic;
this.blockStatistics = blockStatistics;
this.itemStatistics = itemStatistics;

View File

@@ -69,7 +69,7 @@ public class StatusData {
public float expProgress;
/**
* The player's game mode string (one of "survival", "creative", "adventure", "spectator")
* The player's game mode string (one of "SURVIVAL", "CREATIVE", "ADVENTURE", "SPECTATOR")
*/
@SerializedName("game_mode")
public String gameMode;

View File

@@ -5,24 +5,12 @@ 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;
import java.util.List;
/***
* 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;
public class UserData {
/**
* Stores the user's status data, including health, food, etc.
@@ -52,7 +40,7 @@ public class UserData implements Comparable<UserData> {
* Stores the set of this user's advancements
*/
@SerializedName("advancements")
protected HashSet<AdvancementData> advancementData;
protected List<AdvancementData> advancementData;
/**
* Stores the user's set of statistics
@@ -74,10 +62,8 @@ public class UserData implements Comparable<UserData> {
public UserData(@NotNull StatusData statusData, @NotNull InventoryData inventoryData,
@NotNull InventoryData enderChestData, @NotNull PotionEffectData potionEffectData,
@NotNull HashSet<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
@NotNull List<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;
@@ -91,40 +77,6 @@ public class UserData implements Comparable<UserData> {
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;
}
@@ -141,7 +93,7 @@ public class UserData implements Comparable<UserData> {
return potionEffectData;
}
public HashSet<AdvancementData> getAdvancementData() {
public List<AdvancementData> getAdvancementData() {
return advancementData;
}
@@ -156,4 +108,15 @@ public class UserData implements Comparable<UserData> {
public PersistentDataContainerData getPersistentDataContainerData() {
return persistentDataContainerData;
}
@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);
}
}

View File

@@ -0,0 +1,40 @@
package net.william278.husksync.data;
import org.jetbrains.annotations.NotNull;
import java.util.Date;
import java.util.UUID;
/**
* Represents a uniquely versioned and timestamped snapshot of a user's data
*
* @param versionUUID The unique identifier for this user data version
* @param versionTimestamp An epoch milliseconds timestamp of when this data was created
* @param userData The {@link UserData} that has been versioned
*/
public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
@NotNull UserData userData) implements Comparable<VersionedUserData> {
public VersionedUserData(@NotNull final UUID versionUUID, @NotNull final Date versionTimestamp,
@NotNull UserData userData) {
this.versionUUID = versionUUID;
this.versionTimestamp = versionTimestamp;
this.userData = userData;
}
public static VersionedUserData version(@NotNull UserData userData) {
return new VersionedUserData(UUID.randomUUID(), new Date(), 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 VersionedUserData other) {
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
}
}

View File

@@ -1,6 +1,7 @@
package net.william278.husksync.database;
import net.william278.husksync.data.UserData;
import net.william278.husksync.data.VersionedUserData;
import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
@@ -71,10 +72,8 @@ public abstract class Database {
* @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(";");
return formatStatementTables(new String(resourceReader.getResource(schemaFileName)
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
}
/**
@@ -91,9 +90,9 @@ public abstract class Database {
/**
* Initialize the database and ensure tables are present; create tables if they do not exist.
*
* @return A future returning void when complete
* @return A future returning boolean - if the connection could be established.
*/
public abstract CompletableFuture<Void> initialize();
public abstract boolean initialize();
/**
* Ensure a {@link User} has an entry in the database and that their username is up-to-date
@@ -120,20 +119,20 @@ public abstract class Database {
public abstract CompletableFuture<Optional<User>> getUserByName(@NotNull String username);
/**
* Get the current user data for a given user, if it exists.
* Get the current uniquely versioned 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
* @return an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not
*/
public abstract CompletableFuture<Optional<UserData>> getCurrentUserData(@NotNull User user);
public abstract CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull User user);
/**
* Get all UserData entries for a user from the database.
* Get all {@link VersionedUserData} 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
* @return A future returning a list of a user's {@link VersionedUserData} entries
*/
public abstract CompletableFuture<List<UserData>> getUserData(@NotNull User user);
public abstract CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user);
/**
* Prune user data records for a given user to the maximum value as configured
@@ -148,9 +147,14 @@ public abstract class Database {
* 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
* @param userData The uniquely versioned data to add as a {@link VersionedUserData}
* @return A future returning void when complete
*/
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData);
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull VersionedUserData userData);
/**
* Close the database connection
*/
public abstract void close();
}

View File

@@ -3,19 +3,24 @@ 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.data.VersionedUserData;
import net.william278.husksync.player.User;
import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.time.Instant;
import java.util.*;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
public class MySqlDatabase extends Database {
/**
* MySQL server hostname
*/
@@ -40,9 +45,12 @@ public class MySqlDatabase extends Database {
private final int hikariKeepAliveTime;
private final int hikariConnectionTimeOut;
private static final String DATA_POOL_NAME = "HuskHomesHikariPool";
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
private HikariDataSource dataSource;
/**
* The Hikari data source - a pool of database connections that can be fetched on-demand
*/
private HikariDataSource connectionPool;
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger) {
super(settings.getStringValue(Settings.ConfigOption.DATABASE_PLAYERS_TABLE_NAME),
@@ -69,31 +77,31 @@ public class MySqlDatabase extends Database {
* @throws SQLException if the connection fails for some reason
*/
private Connection getConnection() throws SQLException {
return dataSource.getConnection();
return connectionPool.getConnection();
}
@Override
public CompletableFuture<Void> initialize() {
return CompletableFuture.runAsync(() -> {
public boolean initialize() {
try {
// Create jdbc driver connection url
final String jdbcUrl = "jdbc:mysql://" + mySqlHost + ":" + mySqlPort + "/" + mySqlDatabaseName + mySqlConnectionParameters;
dataSource = new HikariDataSource();
dataSource.setJdbcUrl(jdbcUrl);
connectionPool = new HikariDataSource();
connectionPool.setJdbcUrl(jdbcUrl);
// Authenticate
dataSource.setUsername(mySqlUsername);
dataSource.setPassword(mySqlPassword);
connectionPool.setUsername(mySqlUsername);
connectionPool.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);
connectionPool.setMaximumPoolSize(hikariMaximumPoolSize);
connectionPool.setMinimumIdle(hikariMinimumIdle);
connectionPool.setMaxLifetime(hikariMaximumLifetime);
connectionPool.setKeepaliveTime(hikariKeepAliveTime);
connectionPool.setConnectionTimeout(hikariConnectionTimeOut);
connectionPool.setPoolName(DATA_POOL_NAME);
// Prepare database schema; make tables if they don't exist
try (Connection connection = dataSource.getConnection()) {
try (Connection connection = connectionPool.getConnection()) {
// Load database schema CREATE statements from schema file
final String[] databaseSchema = getSchemaStatements("database/mysql_schema.sql");
try (Statement statement = connection.createStatement()) {
@@ -101,10 +109,14 @@ public class MySqlDatabase extends Database {
statement.execute(tableCreationStatement);
}
}
return true;
} catch (SQLException | IOException e) {
getLogger().log(Level.SEVERE, "An error occurred creating tables on the MySQL database: ", e);
getLogger().log(Level.SEVERE, "Failed to perform database setup: " + e.getMessage());
}
});
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Override
@@ -194,7 +206,7 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<Optional<UserData>> getCurrentUserData(@NotNull User user) {
public CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -206,13 +218,17 @@ public class MySqlDatabase extends Database {
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);
final Blob blob = resultSet.getBlob("data");
final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length());
blob.free();
return Optional.of(new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
UserData.fromJson(new String(Snappy.uncompress(compressedDataJson),
StandardCharsets.UTF_8))));
}
}
} catch (SQLException e) {
} catch (SQLException | IOException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
}
return Optional.empty();
@@ -220,9 +236,9 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<List<UserData>> getUserData(@NotNull User user) {
public CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> {
final ArrayList<UserData> retrievedData = new ArrayList<>();
final List<VersionedUserData> retrievedData = new ArrayList<>();
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `data`
@@ -232,14 +248,19 @@ public class MySqlDatabase extends Database {
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());
final Blob blob = resultSet.getBlob("data");
final byte[] compressedDataJson = blob.getBytes(1, (int) blob.length());
blob.free();
final VersionedUserData data = new VersionedUserData(
UUID.fromString(resultSet.getString("version_uuid")),
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
UserData.fromJson(new String(Snappy.uncompress(compressedDataJson),
StandardCharsets.UTF_8)));
retrievedData.add(data);
}
return retrievedData;
}
} catch (SQLException e) {
} catch (SQLException | IOException e) {
getLogger().log(Level.SEVERE, "Failed to fetch a user's current user data from the database", e);
}
return retrievedData;
@@ -256,7 +277,7 @@ public class MySqlDatabase extends Database {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%data_table%`
WHERE `version_uuid`=?"""))) {
statement.setString(1, dataToDelete.getDataUuidVersion().toString());
statement.setString(1, dataToDelete.versionUUID().toString());
statement.executeUpdate();
}
} catch (SQLException e) {
@@ -268,7 +289,7 @@ public class MySqlDatabase extends Database {
}
@Override
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData) {
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull VersionedUserData userData) {
return CompletableFuture.runAsync(() -> {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
@@ -276,14 +297,25 @@ public class MySqlDatabase extends Database {
(`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.setString(2, userData.versionUUID().toString());
statement.setTimestamp(3, Timestamp.from(userData.versionTimestamp().toInstant()));
statement.setBlob(4, new ByteArrayInputStream(Snappy
.compress(userData.userData().toJson().getBytes(StandardCharsets.UTF_8))));
statement.executeUpdate();
}
} catch (SQLException e) {
} catch (SQLException | IOException e) {
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
}
}).thenRunAsync(() -> pruneUserDataRecords(user).join());
})/*.thenRunAsync(() -> pruneUserDataRecords(user).join())*/;
}
@Override
public void close() {
if (connectionPool != null) {
if (!connectionPool.isClosed()) {
connectionPool.close();
}
}
}
}

View File

@@ -12,12 +12,25 @@ import java.util.concurrent.CompletableFuture;
public class EventListener {
/**
* The plugin instance
*/
private final HuskSync huskSync;
/**
* Set of UUIDs current awaiting item synchronization. Events will be cancelled for these users
*/
private final HashSet<UUID> usersAwaitingSync;
/**
* Whether the plugin is currently being disabled
*/
private boolean disabling;
protected EventListener(@NotNull HuskSync huskSync) {
this.huskSync = huskSync;
this.usersAwaitingSync = new HashSet<>();
this.disabling = false;
}
public final void handlePlayerJoin(@NotNull OnlineUser user) {
@@ -27,7 +40,7 @@ public class EventListener {
userData -> user.setData(userData, huskSync.getSettings()).join(),
() -> huskSync.getDatabase().getCurrentUserData(user).thenAccept(
databaseUserData -> databaseUserData.ifPresent(
data -> user.setData(data, huskSync.getSettings()).join())).join())).thenRunAsync(
data -> user.setData(data.userData(), huskSync.getSettings()).join())).join())).thenRunAsync(
() -> {
huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
usersAwaitingSync.remove(user.uuid);
@@ -36,16 +49,35 @@ public class EventListener {
}
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()));
if (disabling) {
return;
}
user.getUserData().thenAccept(userData -> {
System.out.println(userData.userData().toJson());
huskSync.getRedisManager()
.setUserData(user, userData.userData(), RedisManager.RedisKeyType.SERVER_CHANGE).thenRun(
() -> huskSync.getDatabase().setUserData(user, userData).join());
});
}
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {
if (disabling) {
return;
}
CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->
huskSync.getDatabase().setUserData(user, user.getUserData().join()).join()));
}
public final void handlePluginDisable() {
disabling = true;
huskSync.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user ->
huskSync.getDatabase().setUserData(user, user.getUserData().join()).join());
huskSync.getDatabase().close();
huskSync.getRedisManager().close();
}
public final boolean cancelPlayerEvent(@NotNull OnlineUser user) {
return usersAwaitingSync.contains(user.uuid);
}

View File

@@ -5,7 +5,7 @@ import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
@@ -25,6 +25,22 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<StatusData> getStatus();
/**
* Set the player's {@link StatusData}
*
* @param statusData the player's {@link StatusData}
* @param setHealth whether to set the player's health
* @param setMaxHealth whether to set the player's max health
* @param setHunger whether to set the player's hunger
* @param setExperience whether to set the player's experience
* @param setGameMode whether to set the player's game mode
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
final boolean setHealth, final boolean setMaxHealth,
final boolean setHunger, final boolean setExperience,
final boolean setGameMode, boolean setFlying);
/**
* Get the player's inventory {@link InventoryData} contents
*
@@ -32,6 +48,14 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<InventoryData> getInventory();
/**
* Set the player's {@link InventoryData}
*
* @param inventoryData The player's {@link InventoryData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setInventory(@NotNull InventoryData inventoryData);
/**
* Get the player's ender chest {@link InventoryData} contents
*
@@ -39,6 +63,15 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<InventoryData> getEnderChest();
/**
* Set the player's {@link InventoryData}
*
* @param enderChestData The player's {@link InventoryData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setEnderChest(@NotNull InventoryData enderChestData);
/**
* Get the player's {@link PotionEffectData}
*
@@ -46,12 +79,28 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<PotionEffectData> getPotionEffects();
/**
* Set the player's {@link PotionEffectData}
*
* @param potionEffectData The player's {@link PotionEffectData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData);
/**
* Get the player's set of {@link AdvancementData}
*
* @return the player's set of {@link AdvancementData}
*/
public abstract CompletableFuture<HashSet<AdvancementData>> getAdvancements();
public abstract CompletableFuture<List<AdvancementData>> getAdvancements();
/**
* Set the player's {@link AdvancementData}
*
* @param advancementData List of the player's {@link AdvancementData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData);
/**
* Get the player's {@link StatisticsData}
@@ -60,6 +109,14 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<StatisticsData> getStatistics();
/**
* Set the player's {@link StatisticsData}
*
* @param statisticsData The player's {@link StatisticsData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData);
/**
* Get the player's {@link LocationData}
*
@@ -67,6 +124,14 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<LocationData> getLocation();
/**
* Set the player's {@link LocationData}
*
* @param locationData the player's {@link LocationData}
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setLocation(@NotNull LocationData locationData);
/**
* Get the player's {@link PersistentDataContainerData}
*
@@ -74,6 +139,14 @@ public abstract class OnlineUser extends User {
*/
public abstract CompletableFuture<PersistentDataContainerData> getPersistentDataContainer();
/**
* Set the player's {@link PersistentDataContainerData}
*
* @param persistentDataContainerData The player's {@link PersistentDataContainerData} to set
* @return A future returning void when complete
*/
public abstract CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData);
/**
* Set {@link UserData} to a player
*
@@ -81,7 +154,37 @@ public abstract class OnlineUser extends User {
* @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);
public final CompletableFuture<Void> setData(@NotNull UserData data, @NotNull Settings settings) {
return CompletableFuture.runAsync(() -> {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
setInventory(data.getInventoryData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
setEnderChest(data.getEnderChestData()).join();
}
setStatus(data.getStatusData(), settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)).join();
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
setPotionEffects(data.getPotionEffectData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
setAdvancements(data.getAdvancementData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
setStatistics(data.getStatisticData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
setPersistentDataContainer(data.getPersistentDataContainerData()).join();
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
setLocation(data.getLocationData()).join();
}
});
}
/**
* Dispatch a MineDown-formatted message to this player
@@ -110,10 +213,11 @@ public abstract class OnlineUser extends User {
*
* @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()));
public final CompletableFuture<VersionedUserData> getUserData() {
return CompletableFuture.supplyAsync(
() -> VersionedUserData.version(new UserData(getStatus().join(), getInventory().join(),
getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(),
getStatistics().join(), getLocation().join(), getPersistentDataContainer().join())));
}
}

View File

@@ -4,65 +4,130 @@ import net.william278.husksync.config.Settings;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
import org.xerial.snappy.Snappy;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.exceptions.JedisException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
/**
* Manages the connection to the Redis server, handling the caching of user data
*/
public class RedisManager {
private static final String KEY_NAMESPACE = "husksync:";
private static String clusterId = "";
private final JedisPool jedisPool;
private RedisManager(@NotNull Settings settings) {
private final JedisPoolConfig jedisPoolConfig;
private final String redisHost;
private final int redisPort;
private final String redisPassword;
private final boolean redisUseSsl;
private JedisPool jedisPool;
public 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));
this.redisHost = settings.getStringValue(Settings.ConfigOption.REDIS_HOST);
this.redisPort = settings.getIntegerValue(Settings.ConfigOption.REDIS_PORT);
this.redisPassword = settings.getStringValue(Settings.ConfigOption.REDIS_PASSWORD);
this.redisUseSsl = settings.getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL);
// Configure the jedis pool
this.jedisPoolConfig = new JedisPoolConfig();
this.jedisPoolConfig.setMaxIdle(0);
this.jedisPoolConfig.setTestOnBorrow(true);
this.jedisPoolConfig.setTestOnReturn(true);
}
/**
* Initialize the redis connection pool
*
* @return a future returning void when complete
*/
public CompletableFuture<Boolean> initialize() {
return CompletableFuture.supplyAsync(() -> {
if (redisPassword.isBlank()) {
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisUseSsl);
} else {
jedisPool = new JedisPool(jedisPoolConfig, redisHost, redisPort, 0, redisPassword, redisUseSsl);
}
try {
jedisPool.getResource().ping();
} catch (JedisException e) {
return false;
}
return true;
});
}
/**
* Set a user's data to the Redis server
*
* @param user the user to set data for
* @param userData the user's data to set
* @param redisKeyType the type of key to set the data with. This determines the time to live for the data.
* @return a future returning void when complete
*/
public CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData,
@NotNull RedisKeyType redisKeyType) {
try {
return CompletableFuture.runAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
// Set the user's data as a compressed byte array of the json using Snappy
jedis.setex(getKey(redisKeyType, user.uuid), redisKeyType.timeToLive,
Snappy.compress(userData.toJson().getBytes(StandardCharsets.UTF_8)));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Fetch a user's data from the Redis server
*
* @param user The user to fetch data for
* @param redisKeyType The type of key to fetch
* @return The user's data, if it's present on the database. Otherwise, an empty optional.
*/
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user,
@NotNull RedisKeyType redisKeyType) {
return CompletableFuture.supplyAsync(() -> {
try (Jedis jedis = jedisPool.getResource()) {
final byte[] compressedJson = jedis.get(getKey(redisKeyType, user.uuid));
if (compressedJson == null) {
return Optional.empty();
}
// Use Snappy to decompress the json
return Optional.of(UserData.fromJson(new String(Snappy.uncompress(compressedJson),
StandardCharsets.UTF_8)));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
public void close() {
if (jedisPool != null) {
if (!jedisPool.isClosed()) {
jedisPool.close();
}
}
}
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));
private static byte[] getKey(@NotNull RedisKeyType keyType, @NotNull UUID uuid) {
return (keyType.getKeyPrefix() + ":" + uuid).getBytes(StandardCharsets.UTF_8);
}
public enum RedisKeyType {
@@ -77,7 +142,7 @@ public class RedisManager {
@NotNull
public String getKeyPrefix() {
return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase() + ":";
return KEY_NAMESPACE.toLowerCase() + ":" + clusterId.toLowerCase() + ":" + name().toLowerCase();
}
}