Migrate config to Annotaml

feat/data-edit-commands
William 2 years ago
parent 8c0f7a295f
commit acd97a1cb0

@ -12,6 +12,7 @@ dependencies {
compileOnly 'dev.dejvokep:boosted-yaml:1.3'
compileOnly 'com.zaxxer:HikariCP:5.0.1'
compileOnly 'net.william278:DesertWell:1.1'
compileOnly 'net.william278:Annotaml:2.0'
}
shadowJar {
@ -35,4 +36,5 @@ shadowJar {
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
}

@ -1,12 +1,7 @@
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.kyori.adventure.platform.bukkit.BukkitAudiences;
import net.william278.annotaml.Annotaml;
import net.william278.desertwell.Version;
import net.william278.husksync.command.BukkitCommand;
import net.william278.husksync.command.BukkitCommandType;
@ -45,6 +40,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
@ -102,14 +98,14 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
getLoggingAdapter().log(Level.INFO, "Loading plugin configuration settings & locales...");
initialized.set(reload().join());
if (initialized.get()) {
logger.showDebugLogs(settings.getBooleanValue(Settings.ConfigOption.DEBUG_LOGGING));
logger.showDebugLogs(settings.debugLogging);
getLoggingAdapter().log(Level.INFO, "Successfully loaded plugin configuration settings & locales");
} else {
throw new HuskSyncInitializationException("Failed to load plugin configuration settings and/or locales");
}
// Prepare data adapter
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_COMPRESS_DATA)) {
if (settings.compressData) {
dataAdapter = new CompressedDataAdapter();
} else {
dataAdapter = new JsonDataAdapter();
@ -189,7 +185,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
}
// Check for updates
if (settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
if (settings.checkForUpdates) {
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
getLatestVersionIfOutdated().thenAccept(newestVersion ->
newestVersion.ifPresent(newVersion -> getLoggingAdapter().log(Level.WARNING,
@ -325,11 +321,17 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
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"))));
// Load plugin settings
this.settings = Annotaml.create(new File(getDataFolder(), "config.yml"), new Settings()).get();
// Load locales from language preset default
final Locales languagePresets = Annotaml.create(Locales.class,
Objects.requireNonNull(getResource("locales/" + settings.language + ".yml"))).get();
this.locales = Annotaml.create(new File(getDataFolder(), "messages_" + settings.language + ".yml"),
languagePresets).get();
return true;
} catch (IOException | NullPointerException e) {
} catch (IOException | NullPointerException | InvocationTargetException | IllegalAccessException |
InstantiationException e) {
getLoggingAdapter().log(Level.SEVERE, "Failed to load data from the config", e);
return false;
}

@ -4,7 +4,6 @@ import com.zaxxer.hikari.HikariDataSource;
import me.william278.husksync.bukkit.data.DataSerializer;
import net.william278.hslmigrator.HSLConverter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.player.User;
import org.bukkit.Material;
@ -38,11 +37,11 @@ public class LegacyMigrator extends Migrator {
public LegacyMigrator(@NotNull HuskSync plugin) {
super(plugin);
this.hslConverter = HSLConverter.getInstance();
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
this.sourceHost = plugin.getSettings().mySqlHost;
this.sourcePort = plugin.getSettings().mySqlPort;
this.sourceUsername = plugin.getSettings().mySqlUsername;
this.sourcePassword = plugin.getSettings().mySqlPassword;
this.sourceDatabase = plugin.getSettings().mySqlDatabase;
this.sourcePlayersTable = "husksync_players";
this.sourceDataTable = "husksync_data";
this.minecraftVersion = plugin.getMinecraftVersion().toString();

@ -2,7 +2,6 @@ package net.william278.husksync.migrator;
import com.zaxxer.hikari.HikariDataSource;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.player.User;
import net.william278.mpdbconverter.MPDBConverter;
@ -16,7 +15,9 @@ import org.jetbrains.annotations.NotNull;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
@ -41,11 +42,11 @@ public class MpdbMigrator extends Migrator {
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
super(plugin);
this.mpdbConverter = MPDBConverter.getInstance(mySqlPlayerDataBridge);
this.sourceHost = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.sourcePort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
this.sourceUsername = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
this.sourcePassword = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
this.sourceHost = plugin.getSettings().mySqlHost;
this.sourcePort = plugin.getSettings().mySqlPort;
this.sourceUsername = plugin.getSettings().mySqlUsername;
this.sourcePassword = plugin.getSettings().mySqlPassword;
this.sourceDatabase = plugin.getSettings().mySqlDatabase;
this.sourceInventoryTable = "mpdb_inventory";
this.sourceEnderChestTable = "mpdb_enderchest";
this.sourceExperienceTable = "mpdb_experience";

@ -5,6 +5,7 @@ import de.themoep.minedown.adventure.MineDownParser;
import net.kyori.adventure.audience.Audience;
import net.william278.desertwell.Version;
import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.editor.ItemEditorMenu;
import org.bukkit.*;
@ -83,19 +84,18 @@ public class BukkitPlayer extends OnlineUser {
}
@Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
@NotNull List<StatusDataFlag> statusDataFlags) {
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) {
return CompletableFuture.runAsync(() -> {
double currentMaxHealth = Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.getBaseValue();
if (statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.MAX_HEALTH)) {
if (statusData.maxHealth != 0d) {
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH))
.setBaseValue(statusData.maxHealth);
currentMaxHealth = statusData.maxHealth;
}
}
if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HEALTH)) {
final double currentHealth = player.getHealth();
if (statusData.health != currentHealth) {
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
@ -113,24 +113,24 @@ public class BukkitPlayer extends OnlineUser {
}
player.setHealthScaled(statusData.healthScale != 0D);
}
if (statusDataFlags.contains(StatusDataFlag.SET_HUNGER)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.HUNGER)) {
player.setFoodLevel(statusData.hunger);
player.setSaturation(statusData.saturation);
player.setExhaustion(statusData.saturationExhaustion);
}
if (statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
player.getInventory().setHeldItemSlot(statusData.selectedItemSlot);
}
if (statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.EXPERIENCE)) {
player.setTotalExperience(statusData.totalExperience);
player.setLevel(statusData.expLevel);
player.setExp(statusData.expProgress);
}
if (statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.GAME_MODE)) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () ->
player.setGameMode(GameMode.valueOf(statusData.gameMode)));
}
if (statusDataFlags.contains(StatusDataFlag.SET_FLYING)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> {
if (statusData.isFlying) {
player.setAllowFlight(true);

@ -4,6 +4,7 @@ dependencies {
implementation 'net.kyori:adventure-api:4.11.0'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'dev.dejvokep:boosted-yaml:1.3'
implementation 'net.william278:Annotaml:2.0'
implementation 'net.william278:DesertWell:1.1'
implementation 'net.william278:PagineDown:1.1'
implementation('com.zaxxer:HikariCP:5.0.1') {
@ -33,4 +34,5 @@ shadowJar {
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'net.william278.desertwell', 'net.william278.husksync.libraries.desertwell'
relocate 'net.william278.paginedown', 'net.william278.husksync.libraries.paginedown'
relocate 'net.william278.annotaml', 'net.william278.husksync.libraries.annotaml'
}

@ -1,32 +1,37 @@
package net.william278.husksync.config;
import de.themoep.minedown.adventure.MineDown;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.william278.annotaml.YamlFile;
import net.william278.paginedown.ListOptions;
import org.apache.commons.text.StringEscapeUtils;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
/**
* Loaded locales used by the plugin to display various locales
* Loaded locales used by the plugin to display styled messages
*/
@YamlFile(rootedMap = true, header = """
HuskHomes Locales
Developed by William278
See plugin about menu for international locale credits
Formatted in MineDown: https://github.com/Phoenix616/MineDown
Translate HuskSync: https://william278.net/docs/husksync/Translations""")
public class Locales {
/**
* The raw set of locales loaded from yaml
*/
@NotNull
private final HashMap<String, String> rawLocales;
private Locales(@NotNull YamlDocument localesConfig) {
this.rawLocales = new HashMap<>();
for (String localeId : localesConfig.getRoutesAsStrings(false)) {
rawLocales.put(localeId, localesConfig.getString(localeId));
}
}
public Map<String, String> rawLocales = new HashMap<>();
/**
* Returns an un-formatted locale loaded from the locales file
* Returns a raw, un-formatted locale loaded from the locales file
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @return An {@link Optional} containing the locale corresponding to the id, if it exists
@ -36,7 +41,9 @@ public class Locales {
}
/**
* Returns an un-formatted locale loaded from the locales file, with replacements applied
* Returns a raw, un-formatted locale loaded from the locales file, with replacements applied
* <p>
* Note that replacements will not be MineDown-escaped; use {@link #escapeMineDown(String)} to escape replacements
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @param replacements Ordered array of replacement strings to fill in placeholders with
@ -77,12 +84,13 @@ public class Locales {
* @param replacements Ordered array of replacement strings to fill in placeholders with
* @return the raw locale, with inserted placeholders
*/
@NotNull
private String applyReplacements(@NotNull String rawLocale, @NotNull String... replacements) {
int replacementIndexer = 1;
for (String replacement : replacements) {
String replacementString = "%" + replacementIndexer + "%";
rawLocale = rawLocale.replace(replacementString, replacement);
replacementIndexer = replacementIndexer + 1;
replacementIndexer += 1;
}
return rawLocale;
}
@ -143,14 +151,8 @@ public class Locales {
.setSpaceBeforeFooter(false);
}
/**
* Load the locales from a BoostedYaml {@link YamlDocument} locales file
*
* @param localesConfig The loaded {@link YamlDocument} locales.yml file
* @return the loaded {@link Locales}
*/
public static Locales load(@NotNull YamlDocument localesConfig) {
return new Locales(localesConfig);
@SuppressWarnings("unused")
public Locales() {
}
}

@ -1,276 +1,190 @@
package net.william278.husksync.config;
import dev.dejvokep.boostedyaml.YamlDocument;
import net.william278.annotaml.YamlComment;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
/**
* Settings used for the plugin, as read from the config file
* Plugin settings, read from config.yml
*/
@YamlFile(header = """
HuskSync Config
Developed by William278
Information: https://william278.net/project/husksync
Documentation: https://william278.net/docs/husksync""",
versionField = "config_version", versionNumber = 2)
public class Settings {
/**
* Map of {@link ConfigOption}s read from the config file
*/
private final Map<ConfigOption, Object> configOptions;
// Load the settings from the document
private Settings(@NotNull YamlDocument config) {
this.configOptions = new HashMap<>();
Arrays.stream(ConfigOption.values()).forEach(configOption -> configOptions
.put(configOption, switch (configOption.optionType) {
case BOOLEAN -> configOption.getBooleanValue(config);
case STRING -> configOption.getStringValue(config);
case DOUBLE -> configOption.getDoubleValue(config);
case FLOAT -> configOption.getFloatValue(config);
case INTEGER -> configOption.getIntValue(config);
case STRING_LIST -> configOption.getStringListValue(config);
}));
}
// Top-level settings
public String language = "en-gb";
// Default constructor for empty settings
protected Settings(@NotNull Map<ConfigOption, Object> configOptions) {
this.configOptions = configOptions;
}
@YamlKey("check_for_updates")
public boolean checkForUpdates = true;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a boolean
* @throws ClassCastException if the option is not a boolean
*/
public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
return (Boolean) configOptions.get(option);
}
@YamlKey("cluster_id")
public String clusterId = "";
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a string
* @throws ClassCastException if the option is not a string
*/
public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
return (String) configOptions.get(option);
}
@YamlKey("debug_logging")
public boolean debugLogging = false;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a double
* @throws ClassCastException if the option is not a double
*/
public double getDoubleValue(@NotNull ConfigOption option) throws ClassCastException {
return (Double) configOptions.get(option);
}
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a float
* @throws ClassCastException if the option is not a float
*/
public double getFloatValue(@NotNull ConfigOption option) throws ClassCastException {
return (Float) configOptions.get(option);
}
// Database settings
@YamlComment("Database connection settings")
@YamlKey("database.credentials.host")
public String mySqlHost = "localhost";
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as an integer
* @throws ClassCastException if the option is not an integer
*/
public int getIntegerValue(@NotNull ConfigOption option) throws ClassCastException {
return (Integer) configOptions.get(option);
}
@YamlKey("database.credentials.port")
public int mySqlPort = 3306;
/**
* Get the value of the specified {@link ConfigOption}
*
* @param option the {@link ConfigOption} to check
* @return the value of the {@link ConfigOption} as a string {@link List}
* @throws ClassCastException if the option is not a string list
*/
@SuppressWarnings("unchecked")
public List<String> getStringListValue(@NotNull ConfigOption option) throws ClassCastException {
return (List<String>) configOptions.get(option);
@YamlKey("database.credentials.database")
public String mySqlDatabase = "HuskSync";
@YamlKey("database.mysql.credentials.username")
public String mySqlUsername = "root";
@YamlKey("database.credentials.password")
public String mySqlPassword = "pa55w0rd";
@YamlKey("database.credentials.parameters")
public String mySqlConnectionParameters = "?autoReconnect=true&useSSL=false";
@YamlComment("MySQL connection pool properties")
@YamlKey("database.connection_pool.maximum_pool_size")
public int mySqlConnectionPoolSize = 10;
@YamlKey("database.connection_pool.minimum_idle")
public int mySqlConnectionPoolIdle = 10;
@YamlKey("database.connection_pool.maximum_lifetime")
public long mySqlConnectionPoolLifetime = 1800000;
@YamlKey("database.connection_pool.keepalive_time")
public long mySqlConnectionPoolKeepAlive = 0;
@YamlKey("database.connection_pool.connection_timeout")
public long mySqlConnectionPoolTimeout = 5000;
@YamlKey("database.table_names")
public Map<String, String> tableNames = TableName.getDefaults();
@NotNull
public String getTableName(@NotNull TableName tableName) {
return Optional.ofNullable(tableNames.get(tableName.name().toLowerCase()))
.orElse(tableName.defaultName);
}
/**
* Load the settings from a BoostedYaml {@link YamlDocument} config file
*
* @param config The loaded {@link YamlDocument} config.yml file
* @return the loaded {@link Settings}
*/
public static Settings load(@NotNull YamlDocument config) {
return new Settings(config);
// Redis settings
@YamlComment("Redis connection settings")
@YamlKey("redis.credentials.host")
public String redisHost = "localhost";
@YamlKey("redis.credentials.port")
public int redisPort = 6379;
@YamlKey("redis.credentials.password")
public String redisPassword = "";
@YamlKey("redis.use_ssl")
public boolean redisUseSsl = false;
// Synchronization settings
@YamlComment("Synchronization settings")
@YamlKey("synchronization.max_user_data_snapshots")
public int maxUserDataSnapshots = 5;
@YamlKey("synchronization.save_on_world_save")
public boolean saveOnWorldSave = true;
@YamlKey("synchronization.save_on_death")
public boolean saveOnDeath = false;
@YamlKey("synchronization.compress_data")
public boolean compressData = true;
@YamlKey("synchronization.save_dead_player_inventories")
public boolean saveDeadPlayerInventories = true;
@YamlKey("synchronization.network_latency_milliseconds")
public int networkLatencyMilliseconds = 500;
@YamlKey("synchronization.features")
public Map<String, Boolean> synchronizationFeatures = SynchronizationFeature.getDefaults();
public boolean getSynchronizationFeature(@NotNull SynchronizationFeature feature) {
return Optional.ofNullable(synchronizationFeatures.get(feature.name().toLowerCase()))
.orElse(feature.enabledByDefault);
}
/**
* Represents an option stored by a path in config.yml
* Represents the names of tables in the database
*/
public enum ConfigOption {
LANGUAGE("language", OptionType.STRING, "en-gb"),
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false),
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
DATABASE_NAME("database.credentials.database", OptionType.STRING, "HuskSync"),
DATABASE_USERNAME("database.credentials.username", OptionType.STRING, "root"),
DATABASE_PASSWORD("database.credentials.password", OptionType.STRING, "pa55w0rd"),
DATABASE_CONNECTION_PARAMS("database.credentials.params", OptionType.STRING, "?autoReconnect=true&useSSL=false"),
DATABASE_CONNECTION_POOL_MAX_SIZE("database.connection_pool.maximum_pool_size", OptionType.INTEGER, 10),
DATABASE_CONNECTION_POOL_MIN_IDLE("database.connection_pool.minimum_idle", OptionType.INTEGER, 10),
DATABASE_CONNECTION_POOL_MAX_LIFETIME("database.connection_pool.maximum_lifetime", OptionType.INTEGER, 1800000),
DATABASE_CONNECTION_POOL_KEEPALIVE("database.connection_pool.keepalive_time", OptionType.INTEGER, 0),
DATABASE_CONNECTION_POOL_TIMEOUT("database.connection_pool.connection_timeout", OptionType.INTEGER, 5000),
DATABASE_USERS_TABLE_NAME("database.table_names.users_table", OptionType.STRING, "husksync_users"),
DATABASE_USER_DATA_TABLE_NAME("database.table_names.user_data_table", OptionType.STRING, "husksync_user_data"),
REDIS_HOST("redis.credentials.host", OptionType.STRING, "localhost"),
REDIS_PORT("redis.credentials.port", OptionType.INTEGER, 6379),
REDIS_PASSWORD("redis.credentials.password", OptionType.STRING, ""),
REDIS_USE_SSL("redis.use_ssl", OptionType.BOOLEAN, false),
SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS("synchronization.max_user_data_snapshots", OptionType.INTEGER, 5),
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500),
SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES("synchronization.save_dead_player_inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_MAX_HEALTH("synchronization.features.max_health", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_HUNGER("synchronization.features.hunger", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_EXPERIENCE("synchronization.features.experience", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_POTION_EFFECTS("synchronization.features.potion_effects", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ADVANCEMENTS("synchronization.features.advancements", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_GAME_MODE("synchronization.features.game_mode", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_STATISTICS("synchronization.features.statistics", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER("synchronization.features.persistent_data_container", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_LOCATION("synchronization.features.location", OptionType.BOOLEAN, true);
/**
* The path in the config.yml file to the value
*/
@NotNull
public final String configPath;
/**
* The {@link OptionType} of this option
*/
@NotNull
public final OptionType optionType;
/**
* The default value of this option if not set in config
*/
@Nullable
private final Object defaultValue;
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType, @Nullable Object defaultValue) {
this.configPath = configPath;
this.optionType = optionType;
this.defaultValue = defaultValue;
}
public enum TableName {
USERS("husksync_users"),
USER_DATA("husksync_user_data");
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
this.configPath = configPath;
this.optionType = optionType;
this.defaultValue = null;
}
/**
* Get the value at the path specified (or return default if set), as a string
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a string
*/
public String getStringValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getString(configPath, (String) defaultValue)
: config.getString(configPath);
}
private final String defaultName;
/**
* Get the value at the path specified (or return default if set), as a boolean
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a boolean
*/
public boolean getBooleanValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getBoolean(configPath, (Boolean) defaultValue)
: config.getBoolean(configPath);
TableName(@NotNull String defaultName) {
this.defaultName = defaultName;
}
/**
* Get the value at the path specified (or return default if set), as a double
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a double
*/
public double getDoubleValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getDouble(configPath, (Double) defaultValue)
: config.getDouble(configPath);
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(), defaultName);
}
/**
* Get the value at the path specified (or return default if set), as a float
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a float
*/
public float getFloatValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getFloat(configPath, (Float) defaultValue)
: config.getFloat(configPath);
@SuppressWarnings("unchecked")
private static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
.map(TableName::toEntry)
.toArray(Map.Entry[]::new));
}
}
/**
* Get the value at the path specified (or return default if set), as an int
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as an int
*/
public int getIntValue(@NotNull YamlDocument config) {
return defaultValue != null
? config.getInt(configPath, (Integer) defaultValue)
: config.getInt(configPath);
/**
* Represents enabled synchronisation features
*/
public enum SynchronizationFeature {
INVENTORIES(true),
ENDER_CHESTS(true),
HEALTH(true),
MAX_HEALTH(true),
HUNGER(true),
EXPERIENCE(true),
POTION_EFFECTS(true),
ADVANCEMENTS(true),
GAME_MODE(true),
STATISTICS(true),
PERSISTENT_DATA_CONTAINER(false),
LOCATION(false);
private final boolean enabledByDefault;
SynchronizationFeature(boolean enabledByDefault) {
this.enabledByDefault = enabledByDefault;
}
/**
* Get the value at the path specified (or return default if set), as a string {@link List}
*
* @param config The {@link YamlDocument} config file
* @return the value defined in the config, as a string {@link List}
*/
public List<String> getStringListValue(@NotNull YamlDocument config) {
return config.getStringList(configPath, new ArrayList<>());
private Map.Entry<String, Boolean> toEntry() {
return Map.entry(name().toLowerCase(), enabledByDefault);
}
/**
* Represents the type of the object
*/
public enum OptionType {
BOOLEAN,
STRING,
DOUBLE,
FLOAT,
INTEGER,
STRING_LIST
@SuppressWarnings("unchecked")
private static Map<String, Boolean> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
.map(SynchronizationFeature::toEntry)
.toArray(Map.Entry[]::new));
}
}
}
}

@ -8,29 +8,34 @@ import java.util.List;
/**
* Flags for setting {@link StatusData}, indicating which elements should be synced
*
* @deprecated Use the more direct {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
*/
@Deprecated(since = "2.1")
public enum StatusDataFlag {
SET_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HEALTH),
SET_MAX_HEALTH(Settings.ConfigOption.SYNCHRONIZATION_SYNC_MAX_HEALTH),
SET_HUNGER(Settings.ConfigOption.SYNCHRONIZATION_SYNC_HUNGER),
SET_EXPERIENCE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_EXPERIENCE),
SET_GAME_MODE(Settings.ConfigOption.SYNCHRONIZATION_SYNC_GAME_MODE),
SET_FLYING(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION),
SET_SELECTED_ITEM_SLOT(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES);
SET_HEALTH(Settings.SynchronizationFeature.HEALTH),
SET_MAX_HEALTH(Settings.SynchronizationFeature.MAX_HEALTH),
SET_HUNGER(Settings.SynchronizationFeature.HUNGER),
SET_EXPERIENCE(Settings.SynchronizationFeature.EXPERIENCE),
SET_GAME_MODE(Settings.SynchronizationFeature.GAME_MODE),
SET_FLYING(Settings.SynchronizationFeature.LOCATION),
SET_SELECTED_ITEM_SLOT(Settings.SynchronizationFeature.INVENTORIES);
private final Settings.ConfigOption configOption;
private final Settings.SynchronizationFeature feature;
StatusDataFlag(@NotNull Settings.ConfigOption configOption) {
this.configOption = configOption;
StatusDataFlag(@NotNull Settings.SynchronizationFeature feature) {
this.feature = feature;
}
/**
* Returns all status data flags
*
* @return all status data flags as a list
* @deprecated Use {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
*/
@NotNull
@Deprecated(since = "2.1")
@SuppressWarnings("unused")
public static List<StatusDataFlag> getAll() {
return Arrays.stream(StatusDataFlag.values()).toList();
@ -41,11 +46,13 @@ public enum StatusDataFlag {
*
* @param settings the settings to use for determining which flags are enabled
* @return all status data flags that are enabled for setting
* @deprecated Use {@link Settings#getSynchronizationFeature(Settings.SynchronizationFeature)} instead
*/
@NotNull
@Deprecated(since = "2.1")
public static List<StatusDataFlag> getFromSettings(@NotNull Settings settings) {
return Arrays.stream(StatusDataFlag.values()).filter(
flag -> settings.getBooleanValue(flag.configOption)).toList();
flag -> settings.getSynchronizationFeature(flag.feature)).toList();
}
}

@ -40,9 +40,9 @@ public class MySqlDatabase extends Database {
private final int hikariMaximumPoolSize;
private final int hikariMinimumIdle;
private final int hikariMaximumLifetime;
private final int hikariKeepAliveTime;
private final int hikariConnectionTimeOut;
private final long hikariMaximumLifetime;
private final long hikariKeepAliveTime;
private final long hikariConnectionTimeOut;
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
@ -53,21 +53,21 @@ public class MySqlDatabase extends Database {
public MySqlDatabase(@NotNull Settings settings, @NotNull ResourceReader resourceReader, @NotNull Logger logger,
@NotNull DataAdapter dataAdapter, @NotNull EventCannon eventCannon) {
super(settings.getStringValue(Settings.ConfigOption.DATABASE_USERS_TABLE_NAME),
settings.getStringValue(Settings.ConfigOption.DATABASE_USER_DATA_TABLE_NAME),
Math.max(1, Math.min(20, settings.getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_MAX_USER_DATA_SNAPSHOTS))),
super(settings.getTableName(Settings.TableName.USERS),
settings.getTableName(Settings.TableName.USER_DATA),
Math.max(1, Math.min(20, settings.maxUserDataSnapshots)),
resourceReader, dataAdapter, eventCannon, logger);
this.mySqlHost = settings.getStringValue(Settings.ConfigOption.DATABASE_HOST);
this.mySqlPort = settings.getIntegerValue(Settings.ConfigOption.DATABASE_PORT);
this.mySqlDatabaseName = settings.getStringValue(Settings.ConfigOption.DATABASE_NAME);
this.mySqlUsername = settings.getStringValue(Settings.ConfigOption.DATABASE_USERNAME);
this.mySqlPassword = settings.getStringValue(Settings.ConfigOption.DATABASE_PASSWORD);
this.mySqlConnectionParameters = settings.getStringValue(Settings.ConfigOption.DATABASE_CONNECTION_PARAMS);
this.hikariMaximumPoolSize = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_SIZE);
this.hikariMinimumIdle = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MIN_IDLE);
this.hikariMaximumLifetime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_MAX_LIFETIME);
this.hikariKeepAliveTime = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_KEEPALIVE);
this.hikariConnectionTimeOut = settings.getIntegerValue(Settings.ConfigOption.DATABASE_CONNECTION_POOL_TIMEOUT);
this.mySqlHost = settings.mySqlHost;
this.mySqlPort = settings.mySqlPort;
this.mySqlDatabaseName = settings.mySqlDatabase;
this.mySqlUsername = settings.mySqlUsername;
this.mySqlPassword = settings.mySqlPassword;
this.mySqlConnectionParameters = settings.mySqlConnectionParameters;
this.hikariMaximumPoolSize = settings.mySqlConnectionPoolSize;
this.hikariMinimumIdle = settings.mySqlConnectionPoolIdle;
this.hikariMaximumLifetime = settings.mySqlConnectionPoolLifetime;
this.hikariKeepAliveTime = settings.mySqlConnectionPoolKeepAlive;
this.hikariConnectionTimeOut = settings.mySqlConnectionPoolTimeout;
}
/**

@ -1,11 +1,10 @@
package net.william278.husksync.listener;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.data.DataSaveCause;
import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.data.ItemData;
import net.william278.husksync.editor.ItemEditorMenuType;
import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
import java.util.HashSet;
@ -57,7 +56,7 @@ public abstract class EventListener {
CompletableFuture.runAsync(() -> {
try {
// Hold reading data for the network latency threshold, to ensure the source server has set the redis key
Thread.sleep(Math.max(0, plugin.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS)));
Thread.sleep(Math.max(0, plugin.getSettings().networkLatencyMilliseconds));
} catch (InterruptedException e) {
plugin.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e);
} finally {
@ -170,7 +169,7 @@ public abstract class EventListener {
* @param usersInWorld a list of users in the world that is being saved
*/
protected final void handleAsyncWorldSave(@NotNull List<OnlineUser> usersInWorld) {
if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) {
if (disabling || !plugin.getSettings().saveOnWorldSave) {
return;
}
usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent(

@ -39,9 +39,30 @@ public abstract class OnlineUser extends User {
* @param statusData the player's {@link StatusData}
* @param statusDataFlags the flags to use for setting the status data
* @return a future returning void when complete
* @deprecated Use {@link #setStatus(StatusData, Settings)} instead
*/
@Deprecated(since = "2.1")
public final CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
@NotNull List<StatusDataFlag> statusDataFlags) {
final Settings settings = new Settings();
settings.synchronizationFeatures.put(Settings.SynchronizationFeature.HEALTH.name().toLowerCase(), statusDataFlags.contains(StatusDataFlag.SET_HEALTH));
settings.synchronizationFeatures.put(Settings.SynchronizationFeature.MAX_HEALTH.name().toLowerCase(), statusDataFlags.contains(StatusDataFlag.SET_MAX_HEALTH));
settings.synchronizationFeatures.put(Settings.SynchronizationFeature.HUNGER.name().toLowerCase(), statusDataFlags.contains(StatusDataFlag.SET_HUNGER));
settings.synchronizationFeatures.put(Settings.SynchronizationFeature.EXPERIENCE.name().toLowerCase(), statusDataFlags.contains(StatusDataFlag.SET_EXPERIENCE));
settings.synchronizationFeatures.put(Settings.SynchronizationFeature.INVENTORIES.name().toLowerCase(), statusDataFlags.contains(StatusDataFlag.SET_SELECTED_ITEM_SLOT));
settings.synchronizationFeatures.put(Settings.SynchronizationFeature.LOCATION.name().toLowerCase(), statusDataFlags.contains(StatusDataFlag.SET_GAME_MODE) || statusDataFlags.contains(StatusDataFlag.SET_FLYING));
return setStatus(statusData, settings);
}
/**
* Set the player's {@link StatusData}
*
* @param statusData the player's {@link StatusData}
* @param settings settings, containing information about which features should be synced
* @return a future returning void when complete
*/
public abstract CompletableFuture<Void> setStatus(@NotNull StatusData statusData,
@NotNull List<StatusDataFlag> statusDataFlags);
@NotNull Settings settings);
/**
* Get the player's inventory {@link ItemData} contents
@ -237,27 +258,26 @@ public abstract class OnlineUser extends User {
final UserData finalData = preSyncEvent.getUserData();
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
if (!isOffline() && !preSyncEvent.isCancelled()) {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData)));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
finalData.getEnderChest().ifPresent(itemData -> add(setEnderChest(itemData)));
}
finalData.getStatus().ifPresent(statusData -> add(setStatus(statusData,
StatusDataFlag.getFromSettings(settings))));
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
finalData.getStatus().ifPresent(statusData -> add(setStatus(statusData, settings)));
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
finalData.getPotionEffects().ifPresent(potionEffectData -> add(setPotionEffects(potionEffectData)));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
finalData.getAdvancements().ifPresent(advancementData -> add(setAdvancements(advancementData)));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
finalData.getStatistics().ifPresent(statisticData -> add(setStatistics(statisticData)));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
finalData.getLocation().ifPresent(locationData -> add(setLocation(locationData)));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
finalData.getPersistentDataContainer().ifPresent(persistentDataContainerData ->
add(setPersistentDataContainer(persistentDataContainerData)));
}
@ -294,30 +314,30 @@ public abstract class OnlineUser extends User {
final UserDataBuilder builder = UserData.builder(getMinecraftVersion());
final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{
if (!isOffline()) {
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
if (isDead() && settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
if (isDead() && settings.saveDeadPlayerInventories) {
add(CompletableFuture.runAsync(() -> builder.setInventory(ItemData.empty())));
} else {
add(getInventory().thenAccept(builder::setInventory));
}
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
add(getEnderChest().thenAccept(builder::setEnderChest));
}
add(getStatus().thenAccept(builder::setStatus));
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
add(getPotionEffects().thenAccept(builder::setPotionEffects));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
add(getAdvancements().thenAccept(builder::setAdvancements));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
add(getStatistics().thenAccept(builder::setStatistics));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
add(getLocation().thenAccept(builder::setLocation));
}
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
add(getPersistentDataContainer().thenAccept(builder::setPersistentDataContainer));
}
}

@ -1,7 +1,6 @@
package net.william278.husksync.redis;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.UserData;
import net.william278.husksync.player.User;
import org.jetbrains.annotations.NotNull;
@ -33,13 +32,13 @@ public class RedisManager {
public RedisManager(@NotNull HuskSync plugin) {
this.plugin = plugin;
clusterId = plugin.getSettings().getStringValue(Settings.ConfigOption.CLUSTER_ID);
clusterId = plugin.getSettings().clusterId;
// Set redis credentials
this.redisHost = plugin.getSettings().getStringValue(Settings.ConfigOption.REDIS_HOST);
this.redisPort = plugin.getSettings().getIntegerValue(Settings.ConfigOption.REDIS_PORT);
this.redisPassword = plugin.getSettings().getStringValue(Settings.ConfigOption.REDIS_PASSWORD);
this.redisUseSsl = plugin.getSettings().getBooleanValue(Settings.ConfigOption.REDIS_USE_SSL);
this.redisHost = plugin.getSettings().redisHost;
this.redisPort = plugin.getSettings().redisPort;
this.redisPassword = plugin.getSettings().redisPassword;
this.redisUseSsl = plugin.getSettings().redisUseSsl;
// Configure the jedis pool
this.jedisPoolConfig = new JedisPoolConfig();

@ -1,6 +1,7 @@
package net.william278.husksync.player;
import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.*;
import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.desertwell.Version;
@ -31,7 +32,7 @@ public class DummyPlayer extends OnlineUser {
}
@Override
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull List<StatusDataFlag> statusDataFlags) {
public CompletableFuture<Void> setStatus(@NotNull StatusData statusData, @NotNull Settings settings) {
return CompletableFuture.runAsync(() -> {
// do nothing
});

Loading…
Cancel
Save