mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-27 02:29:10 +00:00
Basic bukkit implementation
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
dependencies {
|
||||
compileOnly project(path: ':common')
|
||||
implementation project(path: ':bukkit')
|
||||
compileOnly project(path: ':common')
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
//relocate 'org.xerial', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.william278.husksync.api;
|
||||
|
||||
public class HuskSyncAPI {
|
||||
}
|
||||
13
build.gradle
13
build.gradle
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.0'
|
||||
id 'org.ajoberstar.grgit' version '4.1.1'
|
||||
id 'com.github.johnrengelman.shadow' version '7.1.2'
|
||||
id 'org.ajoberstar.grgit' version '5.0.0'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ version "$ext.plugin_version+${versionMetadata()}"
|
||||
|
||||
ext {
|
||||
set 'version', version.toString()
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'sqlite_driver_version', sqlite_driver_version.toString()
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
@@ -34,13 +32,6 @@ allprojects {
|
||||
maven { url 'https://jitpack.io' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation('redis.clients:jedis:4.2.3') {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
}
|
||||
|
||||
processResources {
|
||||
filter ReplaceTokens as Class, beginToken: '${', endToken: '}',
|
||||
tokens: rootProject.ext.properties
|
||||
|
||||
@@ -3,19 +3,28 @@ dependencies {
|
||||
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.0'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0'
|
||||
implementation 'net.byteflux:libby-bukkit:1.1.5'
|
||||
|
||||
compileOnly 'redis.clients:jedis:4.2.3'
|
||||
compileOnly 'commons-io:commons-io:2.11.0'
|
||||
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
//relocate 'org.xerial', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.huskhomes.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries'
|
||||
relocate 'de.themoep', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.intellij', 'net.william278.huskhomes.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j'
|
||||
relocate 'com.google', 'net.william278.huskhomes.libraries'
|
||||
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
}
|
||||
244
bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
Normal file
244
bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java
Normal file
@@ -0,0 +1,244 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import dev.dejvokep.boostedyaml.dvs.versioning.BasicVersioning;
|
||||
import dev.dejvokep.boostedyaml.settings.dumper.DumperSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.general.GeneralSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.loader.LoaderSettings;
|
||||
import dev.dejvokep.boostedyaml.settings.updater.UpdaterSettings;
|
||||
import net.william278.husksync.command.BukkitCommand;
|
||||
import net.william278.husksync.command.CommandBase;
|
||||
import net.william278.husksync.command.HuskSyncCommand;
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.listener.BukkitEventListener;
|
||||
import net.william278.husksync.listener.EventListener;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.*;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
|
||||
private Database database;
|
||||
|
||||
private RedisManager redisManager;
|
||||
|
||||
private Logger logger;
|
||||
|
||||
private ResourceReader resourceReader;
|
||||
|
||||
private EventListener eventListener;
|
||||
|
||||
private Settings settings;
|
||||
|
||||
private Locales locales;
|
||||
|
||||
private static BukkitHuskSync instance;
|
||||
|
||||
public static BukkitHuskSync getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
instance = this;
|
||||
/*getLogger().log(Level.INFO, "Loading runtime libraries...");
|
||||
final BukkitLibraryManager libraryManager = new BukkitLibraryManager(this);
|
||||
final Library[] libraries = new Library[]{
|
||||
Library.builder().groupId("redis{}clients")
|
||||
.artifactId("jedis")
|
||||
.version("4.2.3")
|
||||
.id("jedis")
|
||||
.build()
|
||||
};
|
||||
libraryManager.addMavenCentral();
|
||||
Arrays.stream(libraries).forEach(libraryManager::loadLibrary);
|
||||
getLogger().log(Level.INFO, "Successfully loaded runtime libraries.");*/
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
// Process initialization stages
|
||||
CompletableFuture.supplyAsync(() -> {
|
||||
// Set the logging adapter and resource reader
|
||||
this.logger = new BukkitLogger(this.getLogger());
|
||||
this.resourceReader = new BukkitResourceReader(this);
|
||||
|
||||
// Load settings and locales
|
||||
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales...");
|
||||
return reload().thenApply(loadedSettings -> {
|
||||
if (loadedSettings) {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
|
||||
} else {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to load plugin configuration settings and/or locales");
|
||||
}
|
||||
return loadedSettings;
|
||||
}).join();
|
||||
}).thenApply(succeeded -> {
|
||||
// Establish connection to the database
|
||||
this.database = new MySqlDatabase(settings, resourceReader, logger);
|
||||
if (succeeded) {
|
||||
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the database...");
|
||||
final CompletableFuture<Boolean> databaseConnectFuture = new CompletableFuture<>();
|
||||
Bukkit.getScheduler().runTask(this, () -> {
|
||||
final boolean initialized = this.database.initialize();
|
||||
if (!initialized) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the database. "
|
||||
+ "Please check the supplied database credentials in the config file");
|
||||
databaseConnectFuture.completeAsync(() -> false);
|
||||
return;
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the database");
|
||||
databaseConnectFuture.completeAsync(() -> true);
|
||||
});
|
||||
return databaseConnectFuture.join();
|
||||
}
|
||||
return false;
|
||||
}).thenApply(succeeded -> {
|
||||
// Establish connection to the Redis server
|
||||
this.redisManager = new RedisManager(settings);
|
||||
if (succeeded) {
|
||||
getLoggingAdapter().log(Level.INFO, "Attempting to establish connection to the Redis server...");
|
||||
return this.redisManager.initialize().thenApply(initialized -> {
|
||||
if (!initialized) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to establish a connection to the Redis server. "
|
||||
+ "Please check the supplied Redis credentials in the config file");
|
||||
return false;
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully established a connection to the Redis server");
|
||||
return true;
|
||||
}).join();
|
||||
}
|
||||
return false;
|
||||
}).thenApply(succeeded -> {
|
||||
// Register events
|
||||
if (succeeded) {
|
||||
getLoggingAdapter().log(Level.INFO, "Registering events...");
|
||||
this.eventListener = new BukkitEventListener(this);
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully registered events listener");
|
||||
}
|
||||
return succeeded;
|
||||
}).thenApply(succeeded -> {
|
||||
// Register permissions
|
||||
if (succeeded) {
|
||||
getLoggingAdapter().log(Level.INFO, "Registering permissions & commands...");
|
||||
Arrays.stream(Permission.values()).forEach(permission -> getServer().getPluginManager().addPermission(new org.bukkit.permissions.Permission(permission.node, switch (permission.defaultAccess) {
|
||||
case EVERYONE -> PermissionDefault.TRUE;
|
||||
case NOBODY -> PermissionDefault.FALSE;
|
||||
case OPERATORS -> PermissionDefault.OP;
|
||||
})));
|
||||
|
||||
// Register commands
|
||||
final CommandBase[] commands = new CommandBase[]{new HuskSyncCommand(this)};
|
||||
for (CommandBase commandBase : commands) {
|
||||
final PluginCommand pluginCommand = getCommand(commandBase.command);
|
||||
if (pluginCommand != null) {
|
||||
new BukkitCommand(commandBase, this).register(pluginCommand);
|
||||
}
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully registered permissions & commands");
|
||||
}
|
||||
return succeeded;
|
||||
}).thenApply(succeeded -> {
|
||||
// Check for updates
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES) && succeeded) {
|
||||
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
|
||||
new UpdateChecker(getVersion(), getLoggingAdapter()).logToConsole();
|
||||
}
|
||||
return succeeded;
|
||||
}).thenAccept(succeeded -> {
|
||||
// Handle failed initialization
|
||||
if (!succeeded) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " +
|
||||
"The plugin will now be disabled");
|
||||
getServer().getPluginManager().disablePlugin(this);
|
||||
} else {
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (this.eventListener != null) {
|
||||
this.eventListener.handlePluginDisable();
|
||||
}
|
||||
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Set<OnlineUser> getOnlineUsers() {
|
||||
return Bukkit.getOnlinePlayers().stream().map(BukkitPlayer::adapt).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
|
||||
final Player player = Bukkit.getPlayer(uuid);
|
||||
if (player == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(BukkitPlayer.adapt(player));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Database getDatabase() {
|
||||
return database;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull RedisManager getRedisManager() {
|
||||
return redisManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Settings getSettings() {
|
||||
return settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Locales getLocales() {
|
||||
return locales;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull Logger getLoggingAdapter() {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull String getVersion() {
|
||||
return getDescription().getVersion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> reload() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
this.settings = Settings.load(YamlDocument.create(new File(getDataFolder(), "config.yml"), Objects.requireNonNull(resourceReader.getResource("config.yml")), GeneralSettings.builder().setUseDefaults(false).build(), LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.builder().setEncoding(DumperSettings.Encoding.UNICODE).build(), UpdaterSettings.builder().setVersioning(new BasicVersioning("config_version")).build()));
|
||||
|
||||
this.locales = Locales.load(YamlDocument.create(new File(getDataFolder(), "messages-" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"), Objects.requireNonNull(resourceReader.getResource("locales/" + settings.getStringValue(Settings.ConfigOption.LANGUAGE) + ".yml"))));
|
||||
return true;
|
||||
} catch (IOException | NullPointerException e) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Bukkit executor that implements and executes {@link CommandBase}s
|
||||
*/
|
||||
public class BukkitCommand implements CommandExecutor, TabExecutor {
|
||||
|
||||
/**
|
||||
* The {@link CommandBase} that will be executed
|
||||
*/
|
||||
private final CommandBase command;
|
||||
|
||||
/**
|
||||
* The implementing plugin
|
||||
*/
|
||||
private final HuskSync plugin;
|
||||
|
||||
public BukkitCommand(@NotNull CommandBase command, @NotNull HuskSync implementor) {
|
||||
this.command = command;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link PluginCommand} to this implementation
|
||||
*
|
||||
* @param pluginCommand {@link PluginCommand} to register
|
||||
*/
|
||||
public void register(@NotNull PluginCommand pluginCommand) {
|
||||
pluginCommand.setExecutor(this);
|
||||
pluginCommand.setTabCompleter(this);
|
||||
pluginCommand.setPermission(command.permission);
|
||||
pluginCommand.setDescription(command.getDescription());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String label, @NotNull String[] args) {
|
||||
if (sender instanceof Player player) {
|
||||
this.command.onExecute(BukkitPlayer.adapt(player), args);
|
||||
} else {
|
||||
if (command instanceof ConsoleExecutable consoleExecutable) {
|
||||
consoleExecutable.onConsoleExecute(args);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_in_game_only").
|
||||
ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
|
||||
@NotNull String alias, @NotNull String[] args) {
|
||||
if (this.command instanceof TabCompletable tabCompletable) {
|
||||
return tabCompletable.onTabComplete(BukkitPlayer.adapt((Player) sender), args);
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
import org.bukkit.util.io.BukkitObjectOutputStream;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class BukkitSerializer {
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link ItemStack}s
|
||||
*
|
||||
* @param inventoryContents The contents of the inventory
|
||||
* @return The serialized inventory contents
|
||||
*/
|
||||
public static CompletableFuture<String> serializeInventory(ItemStack[] inventoryContents) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return an empty string if there is no inventory item data to serialize
|
||||
if (inventoryContents.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the inventory array to serialize
|
||||
bukkitOutputStream.writeInt(inventoryContents.length);
|
||||
|
||||
// Write each serialize each ItemStack to the output stream
|
||||
for (ItemStack inventoryItem : inventoryContents) {
|
||||
bukkitOutputStream.writeObject(serializeItemStack(inventoryItem));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize item stack data");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized inventory data. Note: empty slots will be represented by {@code null}
|
||||
*
|
||||
* @param inventoryData The serialized {@link ItemStack[]} array
|
||||
* @return The inventory contents as an array of {@link ItemStack}s
|
||||
*/
|
||||
public static CompletableFuture<ItemStack[]> deserializeInventory(String inventoryData) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return empty array if there is no inventory data (set the player as having an empty inventory)
|
||||
if (inventoryData.isEmpty()) {
|
||||
return new ItemStack[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(inventoryData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
ItemStack[] inventoryContents = new ItemStack[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the ItemStacks in the array from deserialized ItemStack data
|
||||
int slotIndex = 0;
|
||||
for (ItemStack ignored : inventoryContents) {
|
||||
inventoryContents[slotIndex] = deserializeItemStack(bukkitInputStream.readObject());
|
||||
slotIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized inventory contents
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new RuntimeException("Failed to deserialize item stack data");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param item The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
private static Map<String, Object> serializeItemStack(ItemStack item) {
|
||||
return item != null ? item.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link ItemStack} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedItemStack The serialized item stack; a String-Object map
|
||||
* @return The deserialized {@link ItemStack}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
private static ItemStack deserializeItemStack(Object serializedItemStack) {
|
||||
return serializedItemStack != null ? ItemStack.deserialize((Map<String, Object>) serializedItemStack) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a serialized array of {@link PotionEffect}s
|
||||
*
|
||||
* @param potionEffects The potion effect array
|
||||
* @return The serialized potion effects
|
||||
*/
|
||||
public static CompletableFuture<String> serializePotionEffects(PotionEffect[] potionEffects) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return an empty string if there are no effects to serialize
|
||||
if (potionEffects.length == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create an output stream that will be encoded into base 64
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
|
||||
try (BukkitObjectOutputStream bukkitOutputStream = new BukkitObjectOutputStream(byteOutputStream)) {
|
||||
// Define the length of the potion effect array to serialize
|
||||
bukkitOutputStream.writeInt(potionEffects.length);
|
||||
|
||||
// Write each serialize each PotionEffect to the output stream
|
||||
for (PotionEffect potionEffect : potionEffects) {
|
||||
bukkitOutputStream.writeObject(serializePotionEffect(potionEffect));
|
||||
}
|
||||
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalArgumentException("Failed to serialize potion effect data");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of ItemStacks from serialized potion effect data
|
||||
*
|
||||
* @param potionEffectData The serialized {@link PotionEffect[]} array
|
||||
* @return The {@link PotionEffect}s
|
||||
*/
|
||||
public static CompletableFuture<PotionEffect[]> deserializePotionEffects(String potionEffectData) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Return empty array if there is no potion effect data (don't apply any effects to the player)
|
||||
if (potionEffectData.isEmpty()) {
|
||||
return new PotionEffect[0];
|
||||
}
|
||||
|
||||
// Create a byte input stream to read the serialized data
|
||||
try (ByteArrayInputStream byteInputStream = new ByteArrayInputStream(Base64Coder.decodeLines(potionEffectData))) {
|
||||
try (BukkitObjectInputStream bukkitInputStream = new BukkitObjectInputStream(byteInputStream)) {
|
||||
// Read the length of the Bukkit input stream and set the length of the array to this value
|
||||
PotionEffect[] potionEffects = new PotionEffect[bukkitInputStream.readInt()];
|
||||
|
||||
// Set the potion effects in the array from deserialized PotionEffect data
|
||||
int potionIndex = 0;
|
||||
for (PotionEffect ignored : potionEffects) {
|
||||
potionEffects[potionIndex] = deserializePotionEffect(bukkitInputStream.readObject());
|
||||
potionIndex++;
|
||||
}
|
||||
|
||||
// Return the finished, serialized potion effect array
|
||||
return potionEffects;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
throw new RuntimeException("Failed to deserialize potion effects", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the serialized version of an {@link ItemStack} as a string to object Map
|
||||
*
|
||||
* @param potionEffect The {@link ItemStack} to serialize
|
||||
* @return The serialized {@link ItemStack}
|
||||
*/
|
||||
@Nullable
|
||||
private static Map<String, Object> serializePotionEffect(PotionEffect potionEffect) {
|
||||
return potionEffect != null ? potionEffect.serialize() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the deserialized {@link PotionEffect} from the Object read from the {@link BukkitObjectInputStream}
|
||||
*
|
||||
* @param serializedPotionEffect The serialized potion effect; a String-Object map
|
||||
* @return The deserialized {@link PotionEffect}
|
||||
*/
|
||||
@SuppressWarnings("unchecked") // Ignore the "Unchecked cast" warning
|
||||
@Nullable
|
||||
private static PotionEffect deserializePotionEffect(Object serializedPotionEffect) {
|
||||
return serializedPotionEffect != null ? new PotionEffect((Map<String, Object>) serializedPotionEffect) : null;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.EventHandler;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.player.PlayerEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements Listener {
|
||||
|
||||
public BukkitEventListener(@NotNull BukkitHuskSync huskSync) {
|
||||
super(huskSync);
|
||||
Bukkit.getServer().getPluginManager().registerEvents(this, huskSync);
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerJoin(@NotNull PlayerJoinEvent event) {
|
||||
super.handlePlayerJoin(BukkitPlayer.adapt(event.getPlayer()));
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onPlayerQuit(@NotNull PlayerQuitEvent event) {
|
||||
super.handlePlayerQuit(BukkitPlayer.adapt(event.getPlayer()));
|
||||
BukkitPlayer.remove(event.getPlayer());
|
||||
}
|
||||
|
||||
@EventHandler
|
||||
public void onWorldSave(@NotNull WorldSaveEvent event) {
|
||||
super.handleWorldSave(event.getWorld().getPlayers().stream().map(BukkitPlayer::adapt)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/*@EventHandler(ignoreCancelled = true)
|
||||
public void onGenericPlayerEvent(@NotNull PlayerEvent event) {
|
||||
if (event instanceof Cancellable) {
|
||||
((Cancellable) event).setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.*;
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.potion.PotionEffectType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Bukkit implementation of an {@link OnlineUser}
|
||||
*/
|
||||
public class BukkitPlayer extends OnlineUser {
|
||||
|
||||
private static final HashMap<UUID, BukkitPlayer> cachedPlayers = new HashMap<>();
|
||||
private final Player player;
|
||||
|
||||
private BukkitPlayer(@NotNull Player player) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public static BukkitPlayer adapt(@NotNull Player player) {
|
||||
if (cachedPlayers.containsKey(player.getUniqueId())) {
|
||||
return cachedPlayers.get(player.getUniqueId());
|
||||
}
|
||||
final BukkitPlayer bukkitPlayer = new BukkitPlayer(player);
|
||||
cachedPlayers.put(player.getUniqueId(), bukkitPlayer);
|
||||
return bukkitPlayer;
|
||||
}
|
||||
|
||||
public static void remove(@NotNull Player player) {
|
||||
cachedPlayers.remove(player.getUniqueId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatusData> getStatus() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final double maxHealth = getMaxHealth(player);
|
||||
return new StatusData(Math.min(player.getHealth(), maxHealth),
|
||||
maxHealth,
|
||||
player.isHealthScaled() ? player.getHealthScale() : 0d,
|
||||
player.getFoodLevel(),
|
||||
player.getSaturation(),
|
||||
player.getExhaustion(),
|
||||
player.getInventory().getHeldItemSlot(),
|
||||
player.getTotalExperience(),
|
||||
player.getLevel(),
|
||||
player.getExp(),
|
||||
player.getGameMode().name(),
|
||||
player.getAllowFlight() && player.isFlying());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
|
||||
boolean setHealth, boolean setMaxHealth,
|
||||
boolean setHunger, boolean setExperience,
|
||||
boolean setGameMode, boolean setFlying) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.getBaseValue();
|
||||
if (setMaxHealth) {
|
||||
if (statusData.maxHealth != 0d) {
|
||||
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
|
||||
.setBaseValue(statusData.maxHealth);
|
||||
currentMaxHealth = statusData.maxHealth;
|
||||
}
|
||||
}
|
||||
if (setHealth) {
|
||||
final double currentHealth = player.getHealth();
|
||||
if (statusData.health != currentHealth) {
|
||||
player.setHealth(currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health);
|
||||
}
|
||||
|
||||
if (statusData.healthScale != 0d) {
|
||||
player.setHealthScale(statusData.healthScale);
|
||||
} else {
|
||||
player.setHealthScale(statusData.maxHealth);
|
||||
}
|
||||
player.setHealthScaled(statusData.healthScale != 0D);
|
||||
}
|
||||
if (setHunger) {
|
||||
player.setFoodLevel(statusData.hunger);
|
||||
player.setSaturation(statusData.saturation);
|
||||
player.setExhaustion(statusData.saturationExhaustion);
|
||||
}
|
||||
if (setExperience) {
|
||||
player.setTotalExperience(statusData.totalExperience);
|
||||
player.setLevel(statusData.expLevel);
|
||||
player.setExp(statusData.expProgress);
|
||||
}
|
||||
if (setGameMode) {
|
||||
player.setGameMode(GameMode.valueOf(statusData.gameMode));
|
||||
}
|
||||
if (setFlying) {
|
||||
if (statusData.isFlying) {
|
||||
player.setAllowFlight(true);
|
||||
player.setFlying(true);
|
||||
}
|
||||
player.setFlying(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<InventoryData> getInventory() {
|
||||
return BukkitSerializer.serializeInventory(player.getInventory().getContents())
|
||||
.thenApply(InventoryData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setInventory(@NotNull InventoryData inventoryData) {
|
||||
return BukkitSerializer.deserializeInventory(inventoryData.serializedInventory).thenAccept(contents ->
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getInventory().setContents(contents)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<InventoryData> getEnderChest() {
|
||||
return BukkitSerializer.serializeInventory(player.getEnderChest().getContents())
|
||||
.thenApply(InventoryData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setEnderChest(@NotNull InventoryData enderChestData) {
|
||||
return BukkitSerializer.deserializeInventory(enderChestData.serializedInventory).thenAccept(contents ->
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getEnderChest().setContents(contents)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PotionEffectData> getPotionEffects() {
|
||||
return BukkitSerializer.serializePotionEffects(player.getActivePotionEffects()
|
||||
.toArray(new PotionEffect[0])).thenApply(PotionEffectData::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPotionEffects(@NotNull PotionEffectData potionEffectData) {
|
||||
return BukkitSerializer.deserializePotionEffects(potionEffectData.serializedPotionEffects).thenAccept(
|
||||
effects -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
for (PotionEffect effect : player.getActivePotionEffects()) {
|
||||
player.removePotionEffect(effect.getType());
|
||||
}
|
||||
for (PotionEffect effect : effects) {
|
||||
player.addPotionEffect(effect);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<List<AdvancementData>> getAdvancements() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
final ArrayList<AdvancementData> advancementData = new ArrayList<>();
|
||||
|
||||
// Iterate through the server advancement set and add all advancements to the list
|
||||
serverAdvancements.forEachRemaining(advancement -> {
|
||||
final AdvancementProgress advancementProgress = player.getAdvancementProgress(advancement);
|
||||
final Map<String, Date> awardedCriteria = new HashMap<>();
|
||||
|
||||
advancementProgress.getAwardedCriteria().forEach(criteriaKey -> awardedCriteria.put(criteriaKey,
|
||||
advancementProgress.getDateAwarded(criteriaKey)));
|
||||
|
||||
// Only save the advancement if criteria has been completed
|
||||
if (!awardedCriteria.isEmpty()) {
|
||||
advancementData.add(new AdvancementData(advancement.getKey().toString(), awardedCriteria));
|
||||
}
|
||||
});
|
||||
return advancementData;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setAdvancements(@NotNull List<AdvancementData> advancementData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Temporarily disable advancement announcing if needed
|
||||
boolean announceAdvancementUpdate = false;
|
||||
if (Boolean.TRUE.equals(player.getWorld().getGameRuleValue(GameRule.ANNOUNCE_ADVANCEMENTS))) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false);
|
||||
announceAdvancementUpdate = true;
|
||||
}
|
||||
final boolean finalAnnounceAdvancementUpdate = announceAdvancementUpdate;
|
||||
|
||||
// Save current experience and level
|
||||
final int experienceLevel = player.getLevel();
|
||||
final float expProgress = player.getExp();
|
||||
|
||||
// Determines whether the experience might have changed warranting an update
|
||||
final AtomicBoolean correctExperience = new AtomicBoolean(false);
|
||||
|
||||
// Apply the advancements to the player
|
||||
final Iterator<Advancement> serverAdvancements = Bukkit.getServer().advancementIterator();
|
||||
while (serverAdvancements.hasNext()) {
|
||||
// Iterate through all advancements
|
||||
final Advancement advancement = serverAdvancements.next();
|
||||
final AdvancementProgress playerProgress = player.getAdvancementProgress(advancement);
|
||||
|
||||
advancementData.stream().filter(record -> record.key.equals(advancement.getKey().toString())).findFirst().ifPresentOrElse(
|
||||
// Award all criteria that the player does not have that they do on the cache
|
||||
record -> {
|
||||
record.completedCriteria.keySet().stream()
|
||||
.filter(criterion -> !playerProgress.getAwardedCriteria().contains(criterion))
|
||||
.forEach(criterion -> {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).awardCriteria(criterion));
|
||||
correctExperience.set(true);
|
||||
});
|
||||
|
||||
// Revoke all criteria that the player does have but should not
|
||||
new ArrayList<>(playerProgress.getAwardedCriteria()).stream().filter(criterion -> !record.completedCriteria.containsKey(criterion))
|
||||
.forEach(criterion -> Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion)));
|
||||
|
||||
},
|
||||
// Revoke the criteria as the player shouldn't have any
|
||||
() -> new ArrayList<>(playerProgress.getAwardedCriteria()).forEach(criterion ->
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(),
|
||||
() -> player.getAdvancementProgress(advancement).revokeCriteria(criterion))));
|
||||
|
||||
// Update the player's experience in case the advancement changed that
|
||||
if (correctExperience.get()) {
|
||||
player.setLevel(experienceLevel);
|
||||
player.setExp(expProgress);
|
||||
correctExperience.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-enable announcing advancements (back on main thread again)
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
|
||||
if (finalAnnounceAdvancementUpdate) {
|
||||
player.getWorld().setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<StatisticsData> getStatistics() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final Map<String, Integer> untypedStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> blockStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> itemStatisticValues = new HashMap<>();
|
||||
final Map<String, Map<String, Integer>> entityStatisticValues = new HashMap<>();
|
||||
|
||||
for (Statistic statistic : Statistic.values()) {
|
||||
switch (statistic.getType()) {
|
||||
case ITEM -> {
|
||||
final Map<String, Integer> itemValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isItem)
|
||||
.filter(itemMaterial -> (player.getStatistic(statistic, itemMaterial)) != 0)
|
||||
.forEach(itemMaterial -> itemValues.put(itemMaterial.name(),
|
||||
player.getStatistic(statistic, itemMaterial)));
|
||||
if (!itemValues.isEmpty()) {
|
||||
itemStatisticValues.put(statistic.name(), itemValues);
|
||||
}
|
||||
}
|
||||
case BLOCK -> {
|
||||
final Map<String, Integer> blockValues = new HashMap<>();
|
||||
Arrays.stream(Material.values()).filter(Material::isBlock)
|
||||
.filter(blockMaterial -> (player.getStatistic(statistic, blockMaterial)) != 0)
|
||||
.forEach(blockMaterial -> blockValues.put(blockMaterial.name(),
|
||||
player.getStatistic(statistic, blockMaterial)));
|
||||
if (!blockValues.isEmpty()) {
|
||||
blockStatisticValues.put(statistic.name(), blockValues);
|
||||
}
|
||||
}
|
||||
case ENTITY -> {
|
||||
final Map<String, Integer> entityValues = new HashMap<>();
|
||||
Arrays.stream(EntityType.values()).filter(EntityType::isAlive)
|
||||
.filter(entityType -> (player.getStatistic(statistic, entityType)) != 0)
|
||||
.forEach(entityType -> entityValues.put(entityType.name(),
|
||||
player.getStatistic(statistic, entityType)));
|
||||
if (!entityValues.isEmpty()) {
|
||||
entityStatisticValues.put(statistic.name(), entityValues);
|
||||
}
|
||||
}
|
||||
case UNTYPED -> {
|
||||
if (player.getStatistic(statistic) != 0) {
|
||||
untypedStatisticValues.put(statistic.name(), player.getStatistic(statistic));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StatisticsData(untypedStatisticValues, blockStatisticValues,
|
||||
itemStatisticValues, entityStatisticValues);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setStatistics(@NotNull StatisticsData statisticsData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
// Set untyped statistics
|
||||
for (String statistic : statisticsData.untypedStatistic.keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), statisticsData.untypedStatistic.get(statistic));
|
||||
}
|
||||
|
||||
// Set block statistics
|
||||
for (String statistic : statisticsData.blockStatistics.keySet()) {
|
||||
for (String blockMaterial : statisticsData.blockStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(blockMaterial),
|
||||
statisticsData.blockStatistics.get(statistic).get(blockMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set item statistics
|
||||
for (String statistic : statisticsData.itemStatistics.keySet()) {
|
||||
for (String itemMaterial : statisticsData.itemStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), Material.valueOf(itemMaterial),
|
||||
statisticsData.itemStatistics.get(statistic).get(itemMaterial));
|
||||
}
|
||||
}
|
||||
|
||||
// Set entity statistics
|
||||
for (String statistic : statisticsData.entityStatistics.keySet()) {
|
||||
for (String entityType : statisticsData.entityStatistics.get(statistic).keySet()) {
|
||||
player.setStatistic(Statistic.valueOf(statistic), EntityType.valueOf(entityType),
|
||||
statisticsData.entityStatistics.get(statistic).get(entityType));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<LocationData> getLocation() {
|
||||
return CompletableFuture.supplyAsync(() ->
|
||||
new LocationData(player.getWorld().getName(), player.getWorld().getUID(), player.getWorld().getEnvironment().name(),
|
||||
player.getLocation().getX(), player.getLocation().getY(), player.getLocation().getZ(),
|
||||
player.getLocation().getYaw(), player.getLocation().getPitch()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setLocation(@NotNull LocationData locationData) {
|
||||
final CompletableFuture<Void> completableFuture = new CompletableFuture<>();
|
||||
AtomicReference<World> bukkitWorld = new AtomicReference<>(Bukkit.getWorld(locationData.worldName));
|
||||
if (bukkitWorld.get() == null) {
|
||||
bukkitWorld.set(Bukkit.getWorld(locationData.worldUuid));
|
||||
}
|
||||
if (bukkitWorld.get() == null) {
|
||||
Bukkit.getWorlds().stream().filter(world -> world.getEnvironment() == World.Environment
|
||||
.valueOf(locationData.worldEnvironment)).findFirst().ifPresent(bukkitWorld::set);
|
||||
}
|
||||
if (bukkitWorld.get() != null) {
|
||||
player.teleport(new Location(bukkitWorld.get(),
|
||||
locationData.x, locationData.y, locationData.z,
|
||||
locationData.yaw, locationData.pitch), PlayerTeleportEvent.TeleportCause.PLUGIN);
|
||||
}
|
||||
CompletableFuture.runAsync(() -> completableFuture.completeAsync(() -> null));
|
||||
return completableFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final PersistentDataContainer container = player.getPersistentDataContainer();
|
||||
if (container.isEmpty()) {
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
}
|
||||
final HashMap<String, Byte[]> persistentDataMap = new HashMap<>();
|
||||
for (NamespacedKey key : container.getKeys()) {
|
||||
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY)));
|
||||
}
|
||||
return new PersistentDataContainerData(persistentDataMap);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData) {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
|
||||
player.getPersistentDataContainer().remove(namespacedKey));
|
||||
persistentDataContainerData.persistentDataMap.keySet().forEach(keyString -> {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
if (key != null) {
|
||||
final byte[] data = ArrayUtils.toPrimitive(persistentDataContainerData
|
||||
.persistentDataMap.get(keyString));
|
||||
player.getPersistentDataContainer().set(key, PersistentDataType.BYTE_ARRAY, data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(@NotNull String node) {
|
||||
return player.hasPermission(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.toComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(mineDown.toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Player}'s maximum health, minus any health boost effects
|
||||
*
|
||||
* @param player The {@link Player} to get the maximum health of
|
||||
* @return The {@link Player}'s max health
|
||||
*/
|
||||
private static double getMaxHealth(@NotNull Player player) {
|
||||
double maxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH)).getBaseValue();
|
||||
|
||||
// If the player has additional health bonuses from synchronised potion effects, subtract these from this number as they are synchronised separately
|
||||
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20D) {
|
||||
PotionEffect healthBoostEffect = player.getPotionEffect(PotionEffectType.HEALTH_BOOST);
|
||||
assert healthBoostEffect != null;
|
||||
double healthBoostBonus = 4 * (healthBoostEffect.getAmplifier() + 1);
|
||||
maxHealth -= healthBoostBonus;
|
||||
}
|
||||
return maxHealth;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitLogger implements Logger {
|
||||
|
||||
private final java.util.logging.Logger logger;
|
||||
|
||||
public BukkitLogger(java.util.logging.Logger logger) {
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message, Exception e) {
|
||||
logger.log(level, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(Level level, String message) {
|
||||
logger.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String message) {
|
||||
logger.info(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(String message) {
|
||||
logger.severe(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(String message) {
|
||||
logger.config(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
public class BukkitResourceReader implements ResourceReader {
|
||||
|
||||
private final BukkitHuskSync plugin;
|
||||
|
||||
public BukkitResourceReader(BukkitHuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull InputStream getResource(String fileName) {
|
||||
return Objects.requireNonNull(plugin.getResource(fileName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull File getDataFolder() {
|
||||
return plugin.getDataFolder();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.HuskSyncBukkit
|
||||
main: net.william278.husksync.BukkitHuskSync
|
||||
api-version: 1.16
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
website: 'https://william278.net'
|
||||
softdepend: [MysqlPlayerDataBridge]
|
||||
softdepend: [ MysqlPlayerDataBridge ]
|
||||
libraries:
|
||||
- 'mysql:mysql-connector-java:8.0.29'
|
||||
commands:
|
||||
husksync:
|
||||
usage: '/husksync <update|info|reload>'
|
||||
@@ -1,23 +0,0 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'org.bstats:bstats-bungeecord:3.0.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'net.byteflux:libby-bungee:1.1.5'
|
||||
|
||||
compileOnly 'net.md-5:bungeecord-api:1.16-R0.5-SNAPSHOT'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.byteflux', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
|
||||
dependencies {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude dependency(':slf4j-api')
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
name: HuskSync
|
||||
version: ${version}
|
||||
main: net.william278.husksync.HuskSyncBungeeCord
|
||||
author: William278
|
||||
description: 'A modern, cross-server player data synchronization system'
|
||||
@@ -4,20 +4,21 @@ dependencies {
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
implementation 'redis.clients:jedis:4.2.3'
|
||||
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly 'org.xerial:sqlite-jdbc:' + sqlite_driver_version
|
||||
compileOnly 'redis.clients:jedis:' + jedis_version
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'org.slf4j', 'net.william278.husksync.libraries.slf4j'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
//relocate 'org.xerial', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.huskhomes.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.huskhomes.libraries'
|
||||
relocate 'de.themoep', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.intellij', 'net.william278.huskhomes.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.huskhomes.libraries'
|
||||
relocate 'org.slf4j', 'net.william278.huskhomes.libraries.slf4j'
|
||||
relocate 'com.google', 'net.william278.huskhomes.libraries'
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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>");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
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()));
|
||||
getStatistics().join(), getLocation().join(), getPersistentDataContainer().join())));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> setPlayerData(@NotNull User user, @NotNull UserData userData,
|
||||
/**
|
||||
* 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()) {
|
||||
jedis.setex(redisKeyType.getKeyPrefix() + user.uuid.toString(),
|
||||
redisKeyType.timeToLive, userData.toJson());
|
||||
// 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;
|
||||
}
|
||||
|
||||
public CompletableFuture<Optional<UserData>> getUserData(@NotNull User user, @NotNull RedisKeyType redisKeyType) {
|
||||
/**
|
||||
* 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 String json = jedis.get(redisKeyType.getKeyPrefix() + user.uuid.toString());
|
||||
if (json == null) {
|
||||
final byte[] compressedJson = jedis.get(getKey(redisKeyType, user.uuid));
|
||||
if (compressedJson == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(UserData.fromJson(json));
|
||||
// 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 static CompletableFuture<RedisManager> initialize(@NotNull Settings settings) {
|
||||
return CompletableFuture.supplyAsync(() -> new RedisManager(settings));
|
||||
public void close() {
|
||||
if (jedisPool != null) {
|
||||
if (!jedisPool.isClosed()) {
|
||||
jedisPool.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -49,3 +49,5 @@ synchronization:
|
||||
statistics: true
|
||||
persistent_data_container: true
|
||||
location: false
|
||||
|
||||
config_version: 1
|
||||
@@ -13,7 +13,7 @@ CREATE TABLE IF NOT EXISTS `%data_table%`
|
||||
`version_uuid` char(36) NOT NULL,
|
||||
`player_uuid` char(36) NOT NULL,
|
||||
`timestamp` datetime NOT NULL,
|
||||
`data` json NOT NULL,
|
||||
`data` mediumblob NOT NULL,
|
||||
|
||||
PRIMARY KEY (`version_uuid`),
|
||||
FOREIGN KEY (`player_uuid`) REFERENCES `%players_table%` (`uuid`) ON DELETE CASCADE
|
||||
14
common/src/main/resources/locales/de-de.yml
Normal file
14
common/src/main/resources/locales/de-de.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
synchronisation_complete: '[Daten synchronisiert!](#00fb9a)'
|
||||
viewing_inventory_of: '[Einsicht in das Inventar von](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[Einsicht in die Endertruhe von](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Die Konfigurations- und Meldungsdateien wurden aktualisiert.](#00fb9a)'
|
||||
error_invalid_syntax: '[Fehler:](#ff3300) [Falsche Syntax. Nutze: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Fehler:](#ff3300) [Dieser Spieler konnte nicht gefunden werden](#ff7e5e)'
|
||||
error_no_permission: '[Fehler:](#ff3300) [Du hast nicht die benötigten Berechtigungen um diesen Befehl auszuführen](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf das Inventar eines Online-Spielers zugreifen](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Fehler:](#ff3300) [Du kannst nicht über HuskSync auf die Endertruhe eines Online-Spielers zugreifen](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Fehler:](#ff3300) [Du kannst nicht auf dein eigenes Inventar zugreifen!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Fehler:](#ff3300) [Du kannst nicht auf deine eigene Endertruhe zugreifen!](#ff7e5e)'
|
||||
error_console_command_only: '[Fehler:](#ff3300) [Dieser Befehl kann nur über die %1% Konsole ausgeführt werden](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Fehler:](#ff3300) [Vorgang konnte nicht verarbeitet werden; Es sind keine Server online, auf denen HuskSync installiert ist. Bitte stelle sicher, dass HuskSync sowohl auf dem Proxy-Server als auch auf allen Servern installiert ist, zwischen denen du Daten synchronisieren möchtest.](#ff7e5e)'
|
||||
error_invalid_cluster: '[Fehler:](#ff3300) [Bitte gib die ID eines gültigen Clusters an.](#ff7e5e)'
|
||||
14
common/src/main/resources/locales/es-es.yml
Normal file
14
common/src/main/resources/locales/es-es.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
synchronisation_complete: '[Datos sincronizados!](#00fb9a)'
|
||||
viewing_inventory_of: '[Viendo el inventario de](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[Viendo el Ender Chest de](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Se ha reiniciado la configuración y los archivos de los mensajes.](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [Sintaxis incorrecta. Uso: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Error:](#ff3300) [No se ha podido encontrar a ese jugador](#ff7e5e)'
|
||||
error_no_permission: '[Error:](#ff3300) [No tienes permiso para ejecutar este comando](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al inventario de un jugador conectado](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [A traves de HuskSync no puedes acceder al Ender Chest de un jugador conectado.](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Error:](#ff3300) [No puedes acceder a tu inventario!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [No puedes acceder a tu Ender Chest!](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [Ese comando solo puede ser ejecutado desde la %1% consola](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Error:](#ff3300) [Ha ocurrido un error mientras se procesaba la acción; no hay servidores online con HusckSync instalado. Por favor, asegúrate que HuskSync está instalado tanto en el proxy como en todos los servidores entre los que quieres sincronizar datos.](#ff7e5e)'
|
||||
error_invalid_cluster: '[Error:](#ff3300) [Por favor, especifica la ID de un cluster válido.](#ff7e5e)'
|
||||
14
common/src/main/resources/locales/ja-jp.yml
Normal file
14
common/src/main/resources/locales/ja-jp.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
synchronisation_complete: '[データが同期されました!](#00fb9a)'
|
||||
viewing_inventory_of: '[%1%](#00fb9a bold) [のインベントリを表示します](#00fb9a) '
|
||||
viewing_ender_chest_of: '[%1%](#00fb9a bold) [のエンダーチェストを表示します](#00fb9a) '
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 設定ファイルとメッセージファイルを再読み込みしました。](#00fb9a)'
|
||||
error_invalid_syntax: '[Error:](#ff3300) [構文が正しくありません。使用法: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Error:](#ff3300) [そのプレイヤーは見つかりませんでした](#ff7e5e)'
|
||||
error_no_permission: '[Error:](#ff3300) [このコマンドを実行する権限がありません](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのインベントリにはアクセスできません](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Error:](#ff3300) [HuskSyncからオンラインプレイヤーのエンダーチェストにはアクセスできません](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Error:](#ff3300) [自分のインベントリにはアクセスできません!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Error:](#ff3300) [自分のエンダーチェストにはアクセスできません!](#ff7e5e)'
|
||||
error_console_command_only: '[Error:](#ff3300) [そのコマンドは%1%コンソールからのみ実行できます](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Error:](#ff3300) [操作の処理に失敗; HuskSyncがインストールされているサーバーがオンラインになっていません。プロキシサーバーとデータを同期させたいすべてのサーバーにHuskSyncがインストールされていることを確認してください。](#ff7e5e)'
|
||||
error_invalid_cluster: '[Error:](#ff3300) [有効なクラスターのIDを指定してください。](#ff7e5e)'
|
||||
14
common/src/main/resources/locales/uk-ua.yml
Normal file
14
common/src/main/resources/locales/uk-ua.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
synchronisation_complete: '[Дані синхронізовано!](#00fb9a)'
|
||||
viewing_inventory_of: '[Переглядання інвентару](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[Переглядання скрині енду](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Перезавантажено конфіґ та файли повідомлень.](#00fb9a)'
|
||||
error_invalid_syntax: '[Помилка:](#ff3300) [Неправильний синтакс. Використання: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Помилка:](#ff3300) [Гравця не знайдено](#ff7e5e)'
|
||||
error_no_permission: '[Помилка:](#ff3300) [Ввас немає дозволу на використання цієї команди](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[Помилка:](#ff3300) [Ви не можете переглядати інвентар гравців, що знаходяться онлайн, з допомогую HuskSync](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[Помилка:](#ff3300) [Ви не можете переглядати скриню енду гравців, що знаходяться онлайн, з допомогую HuskSync](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[Помилка:](#ff3300) [Ви не можете переглядати власний інвентар!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[Помилка:](#ff3300) [Ви не можете переглядати власну скриню енду!](#ff7e5e)'
|
||||
error_console_command_only: '[Помилка:](#ff3300) [Ця команда може бути використана лише з допомогою %1% консолі](#ff7e5e)'
|
||||
error_no_servers_proxied: '[Помилка:](#ff3300) [Не вдалося опрацювати операцію; не знайдено жодного сервера із встановленим HuskSync. Запевніться, будьласка, що HuskSync встановлено на Проксі та усіх серверах між якими ви хочете синхроніхувати дані.](#ff7e5e)'
|
||||
error_invalid_cluster: '[Помилка:](#ff3300) [Зазнчте будь ласка ID слушного кластеру.](#ff7e5e)'
|
||||
14
common/src/main/resources/locales/zh-cn.yml
Normal file
14
common/src/main/resources/locales/zh-cn.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
synchronisation_complete: '[数据同步完成](#00fb9a)'
|
||||
viewing_inventory_of: '[查看玩家背包:](#00fb9a) [%1%](#00fb9a bold)'
|
||||
viewing_ender_chest_of: '[查看玩家末影箱:](#00fb9a) [%1%](#00fb9a bold)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 配置与语言文件重载完成.](#00fb9a)'
|
||||
error_invalid_syntax: '[错误:](#ff3300) [格式错误. 使用方法: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[错误:](#ff3300) [未找到目标玩家](#ff7e5e)'
|
||||
error_no_permission: '[错误:](#ff3300) [你没有权限执行此命令](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家物品栏](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[错误:](#ff3300) [你不能在玩家在线时通过 HuskSync 查看与编辑玩家末影箱](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[错误:](#ff3300) [你不能查看和编辑自己的物品栏!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[错误:](#ff3300) [你不能查看和编辑自己的末影箱!](#ff7e5e)'
|
||||
error_console_command_only: '[错误:](#ff3300) [该命令只能由 %1% 控制台执行](#ff7e5e)'
|
||||
error_no_servers_proxied: '[错误:](#ff3300) [操作处理失败; 没有任何安装了 HuskSync 的后端服务器在线. 请确认 HuskSync 已在 BungeeCord/Velocity 等代理服务器和所有你希望互相同步数据的后端服务器间安装.](#ff7e5e)'
|
||||
error_invalid_cluster: '[错误:](#ff3300) [请指定一个有效的集群(cluster) ID.](#ff7e5e)'
|
||||
14
common/src/main/resources/locales/zh-tw.yml
Normal file
14
common/src/main/resources/locales/zh-tw.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
synchronisation_complete: '[資料已同步!!](#00fb9a)'
|
||||
viewing_inventory_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的背包](#00fb9a)'
|
||||
viewing_ender_chest_of: '[查看](#00fb9a) [%1%](#00fb9a bold) [的終界箱](#00fb9a)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| 已重新載入配置和訊息文件](#00fb9a)'
|
||||
error_invalid_syntax: '[錯誤:](#ff3300) [語法不正確,用法: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[錯誤:](#ff3300) [找不到這位玩家](#ff7e5e)'
|
||||
error_no_permission: '[錯誤:](#ff3300) [您沒有權限執行這個指令](#ff7e5e)'
|
||||
error_cannot_view_inventory_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的背包](#ff7e5e)'
|
||||
error_cannot_view_ender_chest_online: '[錯誤:](#ff3300) [您無法通過 HuskSync 查看在線玩家的終界箱](#ff7e5e)'
|
||||
error_cannot_view_own_inventory: '[錯誤:](#ff3300) [您無法查看自己的背包!](#ff7e5e)'
|
||||
error_cannot_view_own_ender_chest: '[錯誤:](#ff3300) [你無法查看自己的終界箱!](#ff7e5e)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能通過 %1% 控制台運行](#ff7e5e)'
|
||||
error_no_servers_proxied: '[錯誤:](#ff3300) [處理操作失敗: 沒有安裝 HuskSync 的伺服器在線。 請確保在 Proxy 伺服器和您希望在其他同步數據的所有伺服器上都安裝了 HuskSync。](#ff7e5e)'
|
||||
error_invalid_cluster: '[錯誤:](#ff3300) [請提供有效的 Cluster ID](#ff7e5e)'
|
||||
@@ -3,8 +3,5 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
javaVersion=16
|
||||
|
||||
plugin_version=1.5
|
||||
plugin_version=2.0
|
||||
plugin_archive=husksync
|
||||
|
||||
jedis_version=4.2.3
|
||||
sqlite_driver_version=3.36.0.3
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
implementation project(path: ':bukkit', configuration: 'shadow')
|
||||
//implementation project(path: ':api', configuration: 'shadow')
|
||||
implementation project(path: ':api', configuration: 'shadow')
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
dependencies {
|
||||
implementation project(path: ':common')
|
||||
|
||||
implementation 'com.zaxxer:HikariCP:5.0.1'
|
||||
implementation 'org.bstats:bstats-velocity:3.0.0'
|
||||
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
|
||||
implementation 'net.byteflux:libby-velocity:1.1.5'
|
||||
|
||||
compileOnly 'com.velocitypowered:velocity-api:3.1.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.byteflux', 'net.william278.husksync.libraries'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
|
||||
dependencies {
|
||||
//noinspection GroovyAssignabilityCheck
|
||||
exclude dependency(':slf4j-api')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user