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

refactor: Migrate from BoostedYaml to Exll's ConfigLib (#233)

* feat: start work on moving to Exll's configlib

* refactor: Fully migrate to Exlll's configlib

* refactor: Optimize imports
This commit is contained in:
William
2024-01-25 15:37:04 +00:00
committed by GitHub
parent a9bd4dd2f0
commit 530b3ef24d
49 changed files with 1652 additions and 1703 deletions

View File

@@ -24,11 +24,11 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kyori.adventure.audience.Audience;
import net.kyori.adventure.platform.AudienceProvider;
import net.william278.annotaml.Annotaml;
import net.william278.desertwell.util.ThrowingConsumer;
import net.william278.desertwell.util.UpdateChecker;
import net.william278.desertwell.util.Version;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.config.ConfigProvider;
import net.william278.husksync.config.Locales;
import net.william278.husksync.config.Server;
import net.william278.husksync.config.Settings;
@@ -47,9 +47,7 @@ import net.william278.husksync.util.Task;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
@@ -57,7 +55,7 @@ import java.util.logging.Level;
/**
* Abstract implementation of the HuskSync plugin.
*/
public interface HuskSync extends Task.Supplier, EventDispatcher {
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider {
int SPIGOT_RESOURCE_ID = 97144;
@@ -195,7 +193,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
@NotNull
String getServerName();
void setServer(@NotNull Server server);
void setServerName(@NotNull Server serverName);
/**
* Returns the plugin {@link Locales}
@@ -247,7 +245,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
* @param throwable a throwable to log
*/
default void debug(@NotNull String message, @NotNull Throwable... throwable) {
if (getSettings().doDebugLogging()) {
if (getSettings().isDebugLogging()) {
log(Level.INFO, getDebugString(message), throwable);
}
}
@@ -320,37 +318,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
*/
Optional<LegacyConverter> getLegacyConverter();
/**
* Reloads the {@link Settings} and {@link Locales} from their respective config files.
*/
default void loadConfigs() {
try {
// Load settings
setSettings(Annotaml.create(
new File(getDataFolder(), "config.yml"),
Settings.class
).get());
// Load server name
setServer(Annotaml.create(
new File(getDataFolder(), "server.yml"),
Server.getDefault(this)
).get());
// Load locales from language preset default
final Locales languagePresets = Annotaml.create(
Locales.class,
Objects.requireNonNull(getResource(String.format("locales/%s.yml", getSettings().getLanguage())))
).get();
setLocales(Annotaml.create(new File(
getDataFolder(),
String.format("messages_%s.yml", getSettings().getLanguage())
), languagePresets).get());
} catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
throw new FailedToLoadException("Failed to load config or message files", e);
}
}
@NotNull
default UpdateChecker getUpdateChecker() {
return UpdateChecker.builder()
@@ -361,7 +328,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher {
}
default void checkForUpdates() {
if (getSettings().doCheckForUpdates()) {
if (getSettings().isCheckForUpdates()) {
getUpdateChecker().check().thenAccept(checked -> {
if (!checked.isUpToDate()) {
log(Level.WARNING, String.format(

View File

@@ -81,9 +81,11 @@ public class EnderChestCommand extends ItemsCommand {
// Create and pack the snapshot with the updated enderChest
final DataSnapshot.Packed snapshot = latestData.get().copy();
snapshot.edit(plugin, (data) -> {
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND));
data.getEnderChest().ifPresent(enderChest -> enderChest.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.ENDERCHEST_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
);
});
plugin.getDatabase().addSnapshot(user, snapshot);
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);

View File

@@ -109,7 +109,9 @@ public class HuskSyncCommand extends Command implements TabProvider {
}
case "reload" -> {
try {
plugin.loadConfigs();
plugin.loadSettings();
plugin.loadLocales();
plugin.loadServer();
plugin.getLocales().getLocale("reload_complete").ifPresent(executor::sendMessage);
} catch (Throwable e) {
executor.sendMessage(new MineDown(
@@ -206,19 +208,31 @@ public class HuskSyncCommand extends Command implements TabProvider {
MINECRAFT_VERSION(plugin -> Component.text(plugin.getMinecraftVersion().toString())),
JAVA_VERSION(plugin -> Component.text(System.getProperty("java.version"))),
JAVA_VENDOR(plugin -> Component.text(System.getProperty("java.vendor"))),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(plugin.getSettings().getSyncMode().toString()))),
DELAY_LATENCY(plugin -> Component.text(plugin.getSettings().getNetworkLatencyMilliseconds() + "ms")),
SYNC_MODE(plugin -> Component.text(WordUtils.capitalizeFully(
plugin.getSettings().getSynchronization().getMode().toString()
))),
DELAY_LATENCY(plugin -> Component.text(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() + "ms"
)),
SERVER_NAME(plugin -> Component.text(plugin.getServerName())),
DATABASE_TYPE(plugin -> Component.text(plugin.getSettings().getDatabaseType().getDisplayName())),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getMySqlHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean(!plugin.getSettings().getRedisSentinelMaster().isBlank())),
USING_REDIS_PASSWORD(plugin -> getBoolean(!plugin.getSettings().getRedisPassword().isBlank())),
REDIS_USING_SSL(plugin -> getBoolean(plugin.getSettings().redisUseSsl())),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getRedisHost())),
DATABASE_TYPE(plugin -> Component.text(plugin.getSettings().getDatabase().getType().getDisplayName())),
IS_DATABASE_LOCAL(plugin -> getLocalhostBoolean(plugin.getSettings().getDatabase().getCredentials().getHost())),
USING_REDIS_SENTINEL(plugin -> getBoolean(
!plugin.getSettings().getRedis().getSentinel().getMaster().isBlank()
)),
USING_REDIS_PASSWORD(plugin -> getBoolean(
!plugin.getSettings().getRedis().getCredentials().getPassword().isBlank()
)),
REDIS_USING_SSL(plugin -> getBoolean(
plugin.getSettings().getRedis().getCredentials().isUseSsl()
)),
IS_REDIS_LOCAL(plugin -> getLocalhostBoolean(
plugin.getSettings().getRedis().getCredentials().getHost()
)),
DATA_TYPES(plugin -> Component.join(
JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> {
boolean enabled = plugin.getSettings().isSyncFeatureEnabled(i);
boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i);
return Component.textOfChildren(Component
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌')))
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)

View File

@@ -81,9 +81,11 @@ public class InventoryCommand extends ItemsCommand {
// Create and pack the snapshot with the updated inventory
final DataSnapshot.Packed snapshot = latestData.get().copy();
snapshot.edit(plugin, (data) -> {
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND));
data.getInventory().ifPresent(inventory -> inventory.setContents(items));
data.setSaveCause(DataSnapshot.SaveCause.INVENTORY_COMMAND);
data.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
);
});
plugin.getDatabase().addSnapshot(user, snapshot);
plugin.getRedisManager().sendUserDataUpdate(user, snapshot);

View File

@@ -147,7 +147,9 @@ public class UserDataCommand extends Command implements TabProvider {
data.edit(plugin, (unpacked -> {
unpacked.getHealth().ifPresent(status -> status.setHealth(Math.max(1, status.getHealth())));
unpacked.setSaveCause(DataSnapshot.SaveCause.BACKUP_RESTORE);
unpacked.setPinned(plugin.getSettings().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE));
unpacked.setPinned(
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.BACKUP_RESTORE)
);
}));
// Set the user's data and send a message

View File

@@ -0,0 +1,153 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.config;
import de.exlll.configlib.NameFormatters;
import de.exlll.configlib.YamlConfigurationProperties;
import de.exlll.configlib.YamlConfigurationStore;
import de.exlll.configlib.YamlConfigurations;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.NotNull;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.logging.Level;
/**
* Interface for getting and setting data from plugin configuration files
*
* @since 1.0
*/
public interface ConfigProvider {
@NotNull
YamlConfigurationProperties.Builder<?> YAML_CONFIGURATION_PROPERTIES = YamlConfigurationProperties.newBuilder()
.setNameFormatter(NameFormatters.LOWER_UNDERSCORE);
/**
* Get the plugin settings, read from the config file
*
* @return the plugin settings
* @since 1.0
*/
@NotNull
Settings getSettings();
/**
* Set the plugin settings
*
* @param settings The settings to set
* @since 1.0
*/
void setSettings(@NotNull Settings settings);
/**
* Load the plugin settings from the config file
*
* @since 1.0
*/
default void loadSettings() {
setSettings(YamlConfigurations.update(
getConfigDirectory().resolve("config.yml"),
Settings.class,
YAML_CONFIGURATION_PROPERTIES.header(Settings.CONFIG_HEADER).build()
));
}
/**
* Get the locales for the plugin
*
* @return the locales for the plugin
* @since 1.0
*/
@NotNull
Locales getLocales();
/**
* Set the locales for the plugin
*
* @param locales The locales to set
* @since 1.0
*/
void setLocales(@NotNull Locales locales);
/**
* Load the locales from the config file
*
* @since 1.0
*/
default void loadLocales() {
final YamlConfigurationStore<Locales> store = new YamlConfigurationStore<>(
Locales.class, YAML_CONFIGURATION_PROPERTIES.header(Locales.CONFIG_HEADER).build()
);
// Read existing locales if present
final Path path = getConfigDirectory().resolve(String.format("messages-%s.yml", getSettings().getLanguage()));
if (!Files.exists(path)) {
setLocales(store.load(path));
return;
}
// Otherwise, save and read the default locales
try (InputStream input = getResource(String.format("locales/%s.yml", getSettings().getLanguage()))) {
final Locales locales = store.read(input);
store.save(locales, path);
setLocales(locales);
} catch (Throwable e) {
getPlugin().log(Level.SEVERE, "An error occurred loading the locales (invalid lang code?)", e);
}
}
@NotNull
String getServerName();
void setServerName(@NotNull Server server);
default void loadServer() {
setServerName(YamlConfigurations.update(
getConfigDirectory().resolve("server.yml"),
Server.class,
YAML_CONFIGURATION_PROPERTIES.header(Server.CONFIG_HEADER).build()
));
}
/**
* Get a plugin resource
*
* @param name The name of the resource
* @return the resource, if found
* @since 1.0
*/
InputStream getResource(@NotNull String name);
/**
* Get the plugin config directory
*
* @return the plugin config directory
* @since 1.0
*/
@NotNull
Path getConfigDirectory();
@NotNull
HuskSync getPlugin();
}

View File

@@ -19,53 +19,60 @@
package net.william278.husksync.config;
import de.exlll.configlib.Configuration;
import de.themoep.minedown.adventure.MineDown;
import net.william278.annotaml.YamlFile;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
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;
import java.util.TreeMap;
/**
* Loaded locales used by the plugin to display styled messages
* Plugin locale configuration
*
* @since 1.0
*/
@YamlFile(rootedMap = true, header = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync 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""")
@SuppressWarnings("FieldMayBeFinal")
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Locales {
/**
* The raw set of locales loaded from yaml
*/
@NotNull
public Map<String, String> rawLocales = new HashMap<>();
static final String CONFIG_HEADER = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync - 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""";
protected static final String DEFAULT_LOCALE = "en-gb";
// The raw set of locales loaded from yaml
Map<String, String> locales = new TreeMap<>();
/**
* Returns a raw, unformatted 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
*/
public Optional<String> getRawLocale(@NotNull String localeId) {
return Optional.ofNullable(rawLocales.get(localeId)).map(StringEscapeUtils::unescapeJava);
return Optional.ofNullable(locales.get(localeId)).map(StringEscapeUtils::unescapeJava);
}
/**
* Returns a raw, unformatted 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
* Note that replacements will not be MineDown-escaped; use {@link #escapeText(String)} to escape replacements
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @param replacements An ordered array of replacement strings to fill in placeholders with
* @param replacements Ordered array of replacement strings to fill in placeholders with
* @return An {@link Optional} containing the replacement-applied locale corresponding to the id, if it exists
*/
public Optional<String> getRawLocale(@NotNull String localeId, @NotNull String... replacements) {
@@ -73,34 +80,45 @@ public class Locales {
}
/**
* Returns a MineDown-formatted locale from the Locales file
* Returns a MineDown-formatted locale from the locales file
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @return An {@link Optional} containing the formatted locale corresponding to the id, if it exists
*/
public Optional<MineDown> getLocale(@NotNull String localeId) {
return getRawLocale(localeId).map(MineDown::new);
return getRawLocale(localeId).map(this::format);
}
/**
* Returns a MineDown-formatted locale from the Locales file, with replacements applied
* Returns a MineDown-formatted locale from the locales file, with replacements applied
* <p>
* Note that replacements will be MineDown-escaped before application
*
* @param localeId String identifier of the locale, corresponding to a key in the file
* @param replacements An ordered array of replacement strings to fill in placeholders with
* @param replacements Ordered array of replacement strings to fill in placeholders with
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
*/
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeMineDown)
.toArray(String[]::new)).map(MineDown::new);
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeText)
.toArray(String[]::new)).map(this::format);
}
/**
* Returns a MineDown-formatted string
*
* @param text The text to format
* @return A {@link MineDown} object containing the formatted text
*/
@NotNull
public MineDown format(@NotNull String text) {
return new MineDown(text);
}
/**
* Apply placeholder replacements to a raw locale
*
* @param rawLocale The raw, unparsed locale
* @param replacements An ordered array of replacement strings to fill in placeholders with
* @param replacements Ordered array of replacement strings to fill in placeholders with
* @return the raw locale, with inserted placeholders
*/
@NotNull
@@ -116,15 +134,12 @@ public class Locales {
/**
* Escape a string from {@link MineDown} formatting for use in a MineDown-formatted locale
* <p>
* Although MineDown provides {@link MineDown#escape(String)}, that method fails to escape events
* properly when using the escaped string in a replacement, so this is used instead
*
* @param string The string to escape
* @return The escaped string
*/
@NotNull
public static String escapeMineDown(@NotNull String string) {
public static String escapeText(@NotNull String string) {
final StringBuilder value = new StringBuilder();
for (int i = 0; i < string.length(); ++i) {
char c = string.charAt(i);
@@ -137,22 +152,30 @@ public class Locales {
value.append(c);
}
return value.toString();
return value.toString().replace("__", "_\\_");
}
/**
* Truncates a String to a specified length, and appends an ellipsis if it is longer than the specified length
*
* @param string The string to truncate
* @param length The maximum length of the string
* @return The truncated string
*/
@NotNull
public static String truncate(@NotNull String string, int length) {
if (string.length() > length) {
return string.substring(0, length) + "";
public String truncateText(@NotNull String string, int truncateAfter) {
if (string.isBlank()) {
return string;
}
return string;
return string.length() > truncateAfter ? string.substring(0, truncateAfter) + "" : string;
}
@NotNull
public String getNotApplicable() {
return getRawLocale("not_applicable").orElse("N/A");
}
@NotNull
public String getListJoiner() {
return getRawLocale("list_separator").orElse(", ");
}
@NotNull
public String getNone() {
return getRawLocale("none").orElse("(none)");
}
/**
@@ -185,10 +208,6 @@ public class Locales {
.setSpaceBeforeFooter(false);
}
@SuppressWarnings("unused")
public Locales() {
}
/**
* Determines the slot a system notification should be displayed in
*/

View File

@@ -19,64 +19,57 @@
package net.william278.husksync.config;
import net.william278.annotaml.Annotaml;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import net.william278.husksync.HuskSync;
import de.exlll.configlib.Configuration;
import de.exlll.configlib.YamlConfigurations;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.nio.file.Paths;
import java.util.stream.Stream;
/**
* Represents a server on a proxied network.
*/
@YamlFile(header = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync Server ID config ┃
┃ Developed by William278 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ This file should contain the ID of this server as defined in your proxy config.
┗╸ If you join it using /server alpha, then set it to 'alpha' (case-sensitive)""")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Server {
@YamlKey("name")
private String serverName;
static final String CONFIG_HEADER = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync - Server ID ┃
┃ Developed by William278 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ This file should contain the ID of this server as defined in your proxy config.
┗╸ If you join it using /server alpha, then set it to 'alpha' (case-sensitive)""";
private Server(@NotNull String serverName) {
this.serverName = serverName;
}
@SuppressWarnings("unused")
private Server() {
}
private String name = getDefault();
@NotNull
public static Server getDefault(@NotNull HuskSync plugin) {
return new Server(getDefaultServerName(plugin));
public static Server of(@NotNull String name) {
return new Server(name);
}
/**
* Find a sensible default name for the server name property
*/
@NotNull
private static String getDefaultServerName(@NotNull HuskSync plugin) {
try {
// Fetch server default from supported plugins if present
for (String s : List.of("HuskHomes", "HuskTowns")) {
final File serverFile = Path.of(plugin.getDataFolder().getParent(), s, "server.yml").toFile();
if (serverFile.exists()) {
return Annotaml.create(serverFile, Server.class).get().getName();
}
}
// Fetch server default from user dir name
final Path serverDirectory = Path.of(System.getProperty("user.dir"));
return serverDirectory.getFileName().toString().trim();
} catch (Throwable e) {
return "server";
}
private static String getDefault() {
final String serverFolder = System.getProperty("user.dir");
return serverFolder == null ? "server" : Stream
.of("HuskHomes", "HuskTowns", "HuskClaims")
.map(s -> Paths.get(serverFolder, "plugins", s, "server.yml").toFile())
.filter(File::exists).findFirst()
.map(file -> YamlConfigurations.load(
file.toPath(),
Server.class,
ConfigProvider.YAML_CONFIGURATION_PROPERTIES.header(CONFIG_HEADER).build()
))
.map(Server::getName)
.orElse(Path.of(serverFolder).getFileName().toString().trim());
}
@Override
@@ -88,12 +81,4 @@ public class Server {
return super.equals(other);
}
/**
* Proxy-defined name of this server.
*/
@NotNull
public String getName() {
return serverName;
}
}

View File

@@ -19,9 +19,11 @@
package net.william278.husksync.config;
import net.william278.annotaml.YamlComment;
import net.william278.annotaml.YamlFile;
import net.william278.annotaml.YamlKey;
import de.exlll.configlib.Comment;
import de.exlll.configlib.Configuration;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.Identifier;
import net.william278.husksync.database.Database;
@@ -29,441 +31,246 @@ import net.william278.husksync.listener.EventListener;
import net.william278.husksync.sync.DataSyncer;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Plugin settings, read from config.yml
*/
@YamlFile(header = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync Config ┃
┃ Developed by William278 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ Information: https://william278.net/project/husksync
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
┗╸ Documentation: https://william278.net/docs/husksync""")
@SuppressWarnings("FieldMayBeFinal")
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Settings {
// Top-level settings
@YamlComment("Locale of the default language file to use. Docs: https://william278.net/docs/husksync/translations")
@YamlKey("language")
private String language = "en-gb";
protected static final String CONFIG_HEADER = """
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ HuskSync Config ┃
┃ Developed by William278 ┃
┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
┣╸ Information: https://william278.net/project/husksync
┣╸ Config Help: https://william278.net/docs/husksync/config-file/
┗╸ Documentation: https://william278.net/docs/husksync""";
@YamlComment("Whether to automatically check for plugin updates on startup")
@YamlKey("check_for_updates")
// Top-level settings
@Comment("Locale of the default language file to use. Docs: https://william278.net/docs/husksync/translations")
private String language = Locales.DEFAULT_LOCALE;
@Comment("Whether to automatically check for plugin updates on startup")
private boolean checkForUpdates = true;
@YamlComment("Specify a common ID for grouping servers running HuskSync. "
@Comment("Specify a common ID for grouping servers running HuskSync. "
+ "Don't modify this unless you know what you're doing!")
@YamlKey("cluster_id")
private String clusterId = "";
@YamlComment("Enable development debug logging")
@YamlKey("debug_logging")
@Comment("Enable development debug logging")
private boolean debugLogging = false;
@YamlComment("Whether to provide modern, rich TAB suggestions for commands (if available)")
@YamlKey("brigadier_tab_completion")
@Comment("Whether to provide modern, rich TAB suggestions for commands (if available)")
private boolean brigadierTabCompletion = false;
@YamlComment("Whether to enable the Player Analytics hook. Docs: https://william278.net/docs/husksync/plan-hook")
@YamlKey("enable_plan_hook")
@Comment("Whether to enable the Player Analytics hook. Docs: https://william278.net/docs/husksync/plan-hook")
private boolean enablePlanHook = true;
// Database settings
@YamlComment("Type of database to use (MYSQL, MARIADB)")
@YamlKey("database.type")
private Database.Type databaseType = Database.Type.MYSQL;
@Comment("Database settings")
private DatabaseSettings database = new DatabaseSettings();
@YamlComment("Specify credentials here for your MYSQL or MARIADB database")
@YamlKey("database.credentials.host")
private String mySqlHost = "localhost";
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class DatabaseSettings {
@YamlKey("database.credentials.port")
private int mySqlPort = 3306;
@Comment("Type of database to use (MYSQL, MARIADB)")
private Database.Type type = Database.Type.MYSQL;
@YamlKey("database.credentials.database")
private String mySqlDatabase = "HuskSync";
@Comment("Specify credentials here for your MYSQL or MARIADB database")
private DatabaseCredentials credentials = new DatabaseCredentials();
@YamlKey("database.credentials.username")
private String mySqlUsername = "root";
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class DatabaseCredentials {
private String host = "localhost";
private int port = 3306;
private String database = "HuskSync";
private String username = "root";
private String password = "pa55w0rd";
private String parameters = String.join("&",
"?autoReconnect=true", "useSSL=false",
"useUnicode=true", "characterEncoding=UTF-8");
}
@YamlKey("database.credentials.password")
private String mySqlPassword = "pa55w0rd";
@Comment({"MYSQL / MARIADB database Hikari connection pool properties.",
"Don't modify this unless you know what you're doing!"})
private PoolSettings connectionPool = new PoolSettings();
@YamlKey("database.credentials.parameters")
private String mySqlConnectionParameters = "?autoReconnect=true"
+ "&useSSL=false"
+ "&useUnicode=true"
+ "&characterEncoding=UTF-8";
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class PoolSettings {
private int maximumPoolSize = 10;
private int minimumIdle = 10;
private long maximumLifetime = 1800000;
private long keepaliveTime = 0;
private long connectionTimeout = 5000;
}
@YamlComment("MYSQL / MARIADB database Hikari connection pool properties. "
+ "Don't modify this unless you know what you're doing!")
@YamlKey("database.connection_pool.maximum_pool_size")
private int mySqlConnectionPoolSize = 10;
@YamlKey("database.connection_pool.minimum_idle")
private int mySqlConnectionPoolIdle = 10;
@YamlKey("database.connection_pool.maximum_lifetime")
private long mySqlConnectionPoolLifetime = 1800000;
@YamlKey("database.connection_pool.keepalive_time")
private long mySqlConnectionPoolKeepAlive = 0;
@YamlKey("database.connection_pool.connection_timeout")
private long mySqlConnectionPoolTimeout = 5000;
@YamlComment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
@YamlKey("database.table_names")
private Map<String, String> tableNames = TableName.getDefaults();
@Comment("Names of tables to use on your database. Don't modify this unless you know what you're doing!")
@Getter(AccessLevel.NONE)
private Map<String, String> tableNames = Database.TableName.getDefaults();
@NotNull
public String getTableName(@NotNull Database.TableName tableName) {
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.getDefaultName());
}
}
// Redis settings
@YamlComment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
@YamlKey("redis.credentials.host")
private String redisHost = "localhost";
@Comment("Redis settings")
private RedisSettings redis = new RedisSettings();
@YamlKey("redis.credentials.port")
private int redisPort = 6379;
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisSettings {
@YamlKey("redis.credentials.password")
private String redisPassword = "";
@Comment("Specify the credentials of your Redis database here. Set \"password\" to '' if you don't have one")
private RedisCredentials credentials = new RedisCredentials();
@YamlKey("redis.use_ssl")
private boolean redisUseSsl = false;
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisCredentials {
private String host = "localhost";
private int port = 6379;
private String password = "";
private boolean useSsl = false;
}
@YamlComment("If you're using Redis Sentinel, specify the master set name. If you don't know what this is, don't change anything here.")
@YamlKey("redis.sentinel.master")
private String redisSentinelMaster = "";
@Comment({"Advanced configuration for users of Redis sentinel.",
"If you don't know what this is, do not modify anything in this section."})
private RedisSentinel sentinel = new RedisSentinel();
@YamlComment("List of host:port pairs")
@YamlKey("redis.sentinel.nodes")
private List<String> redisSentinelNodes = new ArrayList<>();
@YamlKey("redis.sentinel.password")
private String redisSentinelPassword = "";
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class RedisSentinel {
@Comment("The master set name for the Redis sentinel.")
private String master = "";
@Comment("List of host:port pairs")
private List<String> nodes = new ArrayList<>();
private String password = "";
}
}
// Synchronization settings
@YamlComment("The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks."
+ " Docs: https://william278.net/docs/husksync/sync-modes")
@YamlKey("synchronization.mode")
private DataSyncer.Mode syncMode = DataSyncer.Mode.LOCKSTEP;
@Comment("Redis settings")
private SynchronizationSettings synchronization = new SynchronizationSettings();
@YamlComment("The number of data snapshot backups that should be kept at once per user")
@YamlKey("synchronization.max_user_data_snapshots")
private int maxUserDataSnapshots = 16;
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class SynchronizationSettings {
@YamlComment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
@YamlKey("synchronization.snapshot_backup_frequency")
private int snapshotBackupFrequency = 4;
@Comment("The data synchronization mode to use (LOCKSTEP or DELAY). LOCKSTEP is recommended for most networks."
+ " Docs: https://william278.net/docs/husksync/sync-modes")
private DataSyncer.Mode mode = DataSyncer.Mode.LOCKSTEP;
@YamlComment("List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated)."
+ " Docs: https://william278.net/docs/husksync/data-rotation#save-causes")
@YamlKey("synchronization.auto_pinned_save_causes")
private List<String> autoPinnedSaveCauses = List.of(
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
);
@Comment("The number of data snapshot backups that should be kept at once per user")
private int maxUserDataSnapshots = 16;
@YamlComment("Whether to create a snapshot for users on a world when the server saves that world")
@YamlKey("synchronization.save_on_world_save")
private boolean saveOnWorldSave = true;
@Comment("Number of hours between new snapshots being saved as backups (Use \"0\" to backup all snapshots)")
private int snapshotBackupFrequency = 4;
@YamlComment("Whether to create a snapshot for users when they die (containing their death drops)")
@YamlKey("synchronization.save_on_death.enabled")
private boolean saveOnDeath = false;
@Comment("List of save cause IDs for which a snapshot will be automatically pinned (so it won't be rotated)."
+ " Docs: https://william278.net/docs/husksync/data-rotation#save-causes")
@Getter(AccessLevel.NONE)
private List<String> autoPinnedSaveCauses = List.of(
DataSnapshot.SaveCause.INVENTORY_COMMAND.name(),
DataSnapshot.SaveCause.ENDERCHEST_COMMAND.name(),
DataSnapshot.SaveCause.BACKUP_RESTORE.name(),
DataSnapshot.SaveCause.LEGACY_MIGRATION.name(),
DataSnapshot.SaveCause.MPDB_MIGRATION.name()
);
@YamlComment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
+ " Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
@YamlKey("synchronization.save_on_death.items_to_save")
private DeathItemsMode deathItemsMode = DeathItemsMode.DROPS;
@Comment("Whether to create a snapshot for users on a world when the server saves that world")
private boolean saveOnWorldSave = true;
@YamlComment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
@YamlKey("synchronization.save_on_death.save_empty_items")
private boolean saveEmptyDeathItems = true;
@Comment("Configuration for how and when to sync player data when they die")
private SaveOnDeathSettings saveOnDeath = new SaveOnDeathSettings();
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved.")
@YamlKey("synchronization.save_on_death.sync_dead_players_changing_server")
private boolean synchronizeDeadPlayersChangingServer = true;
@Getter
@Configuration
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class SaveOnDeathSettings {
@Comment("Whether to create a snapshot for users when they die (containing their death drops)")
private boolean enabled = false;
@YamlComment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
@YamlKey("synchronization.compress_data")
private boolean compressData = true;
@Comment("What items to save in death snapshots? (DROPS or ITEMS_TO_KEEP). "
+ " Note that ITEMS_TO_KEEP (suggested for keepInventory servers) requires a Paper 1.19.4+ server.")
private DeathItemsMode itemsToSave = DeathItemsMode.DROPS;
@YamlComment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
@YamlKey("synchronization.notification_display_slot")
private Locales.NotificationSlot notificationSlot = Locales.NotificationSlot.ACTION_BAR;
@Comment("Should a death snapshot still be created even if the items to save on the player's death are empty?")
private boolean saveEmptyItems = true;
@YamlComment("(Experimental) Persist Cartography Table locked maps to let them be viewed on any server")
@YamlKey("synchronization.persist_locked_maps")
private boolean persistLockedMaps = true;
@Comment("Whether dead players who log out and log in to a different server should have their items saved.")
private boolean syncDeadPlayersChangingServer = true;
@YamlComment("Whether to synchronize player max health (requires health syncing to be enabled)")
@YamlKey("synchronization.synchronize_max_health")
private boolean synchronizeMaxHealth = true;
@YamlComment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
+ "pulling data from the database instead (i.e., if the user did not change servers).")
@YamlKey("synchronization.network_latency_milliseconds")
private int networkLatencyMilliseconds = 500;
@YamlComment("Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)")
@YamlKey("synchronization.features")
private Map<String, Boolean> synchronizationFeatures = Identifier.getConfigMap();
@YamlComment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
@YamlKey("synchronization.blacklisted_commands_while_locked")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
@YamlComment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
@YamlKey("synchronization.event_priorities")
private Map<String, String> syncEventPriorities = EventListener.ListenerType.getDefaults();
// Zero-args constructor for instantiation via Annotaml
@SuppressWarnings("unused")
public Settings() {
}
@NotNull
public String getLanguage() {
return language;
}
public boolean doCheckForUpdates() {
return checkForUpdates;
}
@NotNull
public String getClusterId() {
return clusterId;
}
public boolean doDebugLogging() {
return debugLogging;
}
public boolean doBrigadierTabCompletion() {
return brigadierTabCompletion;
}
public boolean usePlanHook() {
return enablePlanHook;
}
@NotNull
public Database.Type getDatabaseType() {
return databaseType;
}
@NotNull
public String getMySqlHost() {
return mySqlHost;
}
public int getMySqlPort() {
return mySqlPort;
}
@NotNull
public String getMySqlDatabase() {
return mySqlDatabase;
}
@NotNull
public String getMySqlUsername() {
return mySqlUsername;
}
@NotNull
public String getMySqlPassword() {
return mySqlPassword;
}
@NotNull
public String getMySqlConnectionParameters() {
return mySqlConnectionParameters;
}
@NotNull
public String getTableName(@NotNull TableName tableName) {
return tableNames.getOrDefault(tableName.name().toLowerCase(Locale.ENGLISH), tableName.defaultName);
}
public int getMySqlConnectionPoolSize() {
return mySqlConnectionPoolSize;
}
public int getMySqlConnectionPoolIdle() {
return mySqlConnectionPoolIdle;
}
public long getMySqlConnectionPoolLifetime() {
return mySqlConnectionPoolLifetime;
}
public long getMySqlConnectionPoolKeepAlive() {
return mySqlConnectionPoolKeepAlive;
}
public long getMySqlConnectionPoolTimeout() {
return mySqlConnectionPoolTimeout;
}
@NotNull
public String getRedisHost() {
return redisHost;
}
public int getRedisPort() {
return redisPort;
}
@NotNull
public String getRedisPassword() {
return redisPassword;
}
public boolean redisUseSsl() {
return redisUseSsl;
}
@NotNull
public String getRedisSentinelMaster() {
return redisSentinelMaster;
}
@NotNull
public List<String> getRedisSentinelNodes() {
return redisSentinelNodes;
}
@NotNull
public String getRedisSentinelPassword() {
return redisSentinelPassword;
}
@NotNull
public DataSyncer.Mode getSyncMode() {
return syncMode;
}
public int getMaxUserDataSnapshots() {
return maxUserDataSnapshots;
}
public int getBackupFrequency() {
return snapshotBackupFrequency;
}
public boolean doSaveOnWorldSave() {
return saveOnWorldSave;
}
public boolean doSaveOnDeath() {
return saveOnDeath;
}
@NotNull
public DeathItemsMode getDeathItemsMode() {
return deathItemsMode;
}
public boolean doSaveEmptyDeathItems() {
return saveEmptyDeathItems;
}
public boolean doCompressData() {
return compressData;
}
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
return autoPinnedSaveCauses.contains(cause.name());
}
@NotNull
public Locales.NotificationSlot getNotificationDisplaySlot() {
return notificationSlot;
}
public boolean doPersistLockedMaps() {
return persistLockedMaps;
}
public boolean doSynchronizeDeadPlayersChangingServer() {
return synchronizeDeadPlayersChangingServer;
}
public boolean doSynchronizeMaxHealth() {
return synchronizeMaxHealth;
}
public int getNetworkLatencyMilliseconds() {
return networkLatencyMilliseconds;
}
@NotNull
public Map<String, Boolean> getSynchronizationFeatures() {
return synchronizationFeatures;
}
public boolean isSyncFeatureEnabled(@NotNull Identifier id) {
return id.isCustom() || getSynchronizationFeatures().getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
}
@NotNull
public List<String> getBlacklistedCommandsWhileLocked() {
return blacklistedCommandsWhileLocked;
}
@NotNull
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try {
return EventListener.Priority.valueOf(syncEventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
} catch (IllegalArgumentException e) {
return EventListener.Priority.NORMAL;
/**
* Represents the mode of saving items on death
*/
public enum DeathItemsMode {
DROPS,
ITEMS_TO_KEEP
}
}
}
/**
* Represents the mode of saving items on death
*/
public enum DeathItemsMode {
DROPS,
ITEMS_TO_KEEP
}
@Comment("Whether to use the snappy data compression algorithm. Keep on unless you know what you're doing")
private boolean compressData = true;
/**
* Represents the names of tables in the database
*/
public enum TableName {
USERS("husksync_users"),
USER_DATA("husksync_user_data");
@Comment("Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)")
private Locales.NotificationSlot notificationDisplaySlot = Locales.NotificationSlot.ACTION_BAR;
private final String defaultName;
@Comment("Persist maps locked in a Cartography Table to let them be viewed on any server")
private boolean persistLockedMaps = true;
TableName(@NotNull String defaultName) {
this.defaultName = defaultName;
@Comment("Whether to synchronize player max health (requires health syncing to be enabled)")
private boolean synchronizeMaxHealth = true;
@Comment("If using the DELAY sync method, how long should this server listen for Redis key data updates before "
+ "pulling data from the database instead (i.e., if the user did not change servers).")
private int networkLatencyMilliseconds = 500;
@Comment("Which data types to synchronize (Docs: https://william278.net/docs/husksync/sync-features)")
@Getter(AccessLevel.NONE)
private Map<String, Boolean> features = Identifier.getConfigMap();
@Comment("Commands which should be blocked before a player has finished syncing (Use * to block all commands)")
private List<String> blacklistedCommandsWhileLocked = new ArrayList<>(List.of("*"));
@Comment("Event priorities for listeners (HIGHEST, NORMAL, LOWEST). Change if you encounter plugin conflicts")
@Getter(AccessLevel.NONE)
private Map<String, String> eventPriorities = EventListener.ListenerType.getDefaults();
public boolean doAutoPin(@NotNull DataSnapshot.SaveCause cause) {
return autoPinnedSaveCauses.contains(cause.name());
}
public boolean isFeatureEnabled(@NotNull Identifier id) {
return id.isCustom() || features.getOrDefault(id.getKeyValue(), id.isEnabledByDefault());
}
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName);
}
@SuppressWarnings("unchecked")
@NotNull
private static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
.map(TableName::toEntry)
.toArray(Map.Entry[]::new));
public EventListener.Priority getEventPriority(@NotNull EventListener.ListenerType type) {
try {
return EventListener.Priority.valueOf(eventPriorities.get(type.name().toLowerCase(Locale.ENGLISH)));
} catch (IllegalArgumentException e) {
return EventListener.Priority.NORMAL;
}
}
}

View File

@@ -26,7 +26,6 @@ import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.config.Locales;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -718,7 +717,7 @@ public class DataSnapshot {
}
return new Unpacked(
id,
pinned || plugin.getSettings().doAutoPin(saveCause),
pinned || plugin.getSettings().getSynchronization().doAutoPin(saveCause),
timestamp,
saveCause,
serverName,
@@ -821,8 +820,7 @@ public class DataSnapshot {
@NotNull
public String getDisplayName() {
return Locales.truncate(name().toLowerCase(Locale.ENGLISH)
.replaceAll("_", " "), 18);
return name().toLowerCase(Locale.ENGLISH);
}
@NotNull

View File

@@ -43,7 +43,7 @@ public interface UserDataHolder extends DataHolder {
@NotNull
default Map<Identifier, Data> getData() {
return getPlugin().getRegisteredDataTypes().stream()
.filter(type -> type.isCustom() || getPlugin().getSettings().isSyncFeatureEnabled(type))
.filter(type -> type.isCustom() || getPlugin().getSettings().getSynchronization().isFeatureEnabled(type))
.map(id -> Map.entry(id, getData(id)))
.filter(data -> data.getValue().isPresent())
.collect(HashMap::new, (map, data) -> map.put(data.getKey(), data.getValue().get()), HashMap::putAll);
@@ -105,7 +105,7 @@ public interface UserDataHolder extends DataHolder {
try {
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
final Identifier identifier = entry.getKey();
if (plugin.getSettings().isSyncFeatureEnabled(identifier)) {
if (plugin.getSettings().getSynchronization().isFeatureEnabled(identifier)) {
if (identifier.isCustom()) {
getCustomDataStore().put(identifier, entry.getValue());
}

View File

@@ -19,6 +19,7 @@
package net.william278.husksync.database;
import lombok.Getter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
@@ -31,10 +32,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.*;
/**
* An abstract representation of the plugin database, storing player data.
@@ -71,8 +69,9 @@ public abstract class Database {
*/
@NotNull
protected final String formatStatementTables(@NotNull String sql) {
return sql.replaceAll("%users_table%", plugin.getSettings().getTableName(Settings.TableName.USERS))
.replaceAll("%user_data_table%", plugin.getSettings().getTableName(Settings.TableName.USER_DATA));
final Settings.DatabaseSettings settings = plugin.getSettings().getDatabase();
return sql.replaceAll("%users_table%", settings.getTableName(TableName.USERS))
.replaceAll("%user_data_table%", settings.getTableName(TableName.USER_DATA));
}
/**
@@ -193,7 +192,7 @@ public abstract class Database {
*/
@Blocking
private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
final int backupFrequency = plugin.getSettings().getBackupFrequency();
final int backupFrequency = plugin.getSettings().getSynchronization().getSnapshotBackupFrequency();
if (!snapshot.isPinned() && backupFrequency > 0) {
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));
}
@@ -297,4 +296,31 @@ public abstract class Database {
}
}
/**
* Represents the names of tables in the database
*/
@Getter
public enum TableName {
USERS("husksync_users"),
USER_DATA("husksync_user_data");
private final String defaultName;
TableName(@NotNull String defaultName) {
this.defaultName = defaultName;
}
@NotNull
private Map.Entry<String, String> toEntry() {
return Map.entry(name().toLowerCase(Locale.ENGLISH), defaultName);
}
@SuppressWarnings("unchecked")
@NotNull
public static Map<String, String> getDefaults() {
return Map.ofEntries(Arrays.stream(values())
.map(TableName::toEntry)
.toArray(Map.Entry[]::new));
}
}
}

View File

@@ -34,6 +34,8 @@ import java.time.OffsetDateTime;
import java.util.*;
import java.util.logging.Level;
import static net.william278.husksync.config.Settings.DatabaseSettings;
public class MySqlDatabase extends Database {
private static final String DATA_POOL_NAME = "HuskSyncHikariPool";
@@ -43,9 +45,10 @@ public class MySqlDatabase extends Database {
public MySqlDatabase(@NotNull HuskSync plugin) {
super(plugin);
this.flavor = plugin.getSettings().getDatabaseType().getProtocol();
this.driverClass = plugin.getSettings().getDatabaseType() == Type.MARIADB
? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
final Type type = plugin.getSettings().getDatabase().getType();
this.flavor = type.getProtocol();
this.driverClass = type == Type.MARIADB ? "org.mariadb.jdbc.Driver" : "com.mysql.cj.jdbc.Driver";
}
/**
@@ -67,26 +70,28 @@ public class MySqlDatabase extends Database {
@Override
public void initialize() throws IllegalStateException {
// Initialize the Hikari pooled connection
final DatabaseSettings.DatabaseCredentials credentials = plugin.getSettings().getDatabase().getCredentials();
dataSource = new HikariDataSource();
dataSource.setDriverClassName(driverClass);
dataSource.setJdbcUrl(String.format("jdbc:%s://%s:%s/%s%s",
flavor,
plugin.getSettings().getMySqlHost(),
plugin.getSettings().getMySqlPort(),
plugin.getSettings().getMySqlDatabase(),
plugin.getSettings().getMySqlConnectionParameters()
credentials.getHost(),
credentials.getPort(),
credentials.getDatabase(),
credentials.getParameters()
));
// Authenticate with the database
dataSource.setUsername(plugin.getSettings().getMySqlUsername());
dataSource.setPassword(plugin.getSettings().getMySqlPassword());
dataSource.setUsername(credentials.getUsername());
dataSource.setPassword(credentials.getPassword());
// Set connection pool options
dataSource.setMaximumPoolSize(plugin.getSettings().getMySqlConnectionPoolSize());
dataSource.setMinimumIdle(plugin.getSettings().getMySqlConnectionPoolIdle());
dataSource.setMaxLifetime(plugin.getSettings().getMySqlConnectionPoolLifetime());
dataSource.setKeepaliveTime(plugin.getSettings().getMySqlConnectionPoolKeepAlive());
dataSource.setConnectionTimeout(plugin.getSettings().getMySqlConnectionPoolTimeout());
final DatabaseSettings.PoolSettings pool = plugin.getSettings().getDatabase().getConnectionPool();
dataSource.setMaximumPoolSize(pool.getMaximumPoolSize());
dataSource.setMinimumIdle(pool.getMinimumIdle());
dataSource.setMaxLifetime(pool.getMaximumLifetime());
dataSource.setKeepaliveTime(pool.getKeepaliveTime());
dataSource.setConnectionTimeout(pool.getConnectionTimeout());
dataSource.setPoolName(DATA_POOL_NAME);
// Set additional connection pool properties
@@ -306,7 +311,8 @@ public class MySqlDatabase extends Database {
protected void rotateSnapshots(@NotNull User user) {
final List<DataSnapshot.Packed> unpinnedUserData = getAllSnapshots(user).stream()
.filter(dataSnapshot -> !dataSnapshot.isPinned()).toList();
if (unpinnedUserData.size() > plugin.getSettings().getMaxUserDataSnapshots()) {
final int maxSnapshots = plugin.getSettings().getSynchronization().getMaxUserDataSnapshots();
if (unpinnedUserData.size() > maxSnapshots) {
try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
DELETE FROM `%user_data_table%`
@@ -314,7 +320,7 @@ public class MySqlDatabase extends Database {
AND `pinned` IS FALSE
ORDER BY `timestamp` ASC
LIMIT %entry_count%;""".replace("%entry_count%",
Integer.toString(unpinnedUserData.size() - plugin.getSettings().getMaxUserDataSnapshots()))))) {
Integer.toString(unpinnedUserData.size() - maxSnapshots))))) {
statement.setString(1, user.getUuid().toString());
statement.executeUpdate();
}

View File

@@ -30,6 +30,8 @@ import java.util.List;
import java.util.Map;
import java.util.UUID;
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
/**
* Handles what should happen when events are fired
*/
@@ -74,7 +76,7 @@ public abstract class EventListener {
* @param usersInWorld a list of users in the world that is being saved
*/
protected final void saveOnWorldSave(@NotNull List<OnlineUser> usersInWorld) {
if (plugin.isDisabling() || !plugin.getSettings().doSaveOnWorldSave()) {
if (plugin.isDisabling() || !plugin.getSettings().getSynchronization().isSaveOnWorldSave()) {
return;
}
usersInWorld.stream()
@@ -91,8 +93,9 @@ public abstract class EventListener {
* @param items The items that should be saved for this user on their death
*/
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull Data.Items items) {
if (plugin.isDisabling() || !plugin.getSettings().doSaveOnDeath() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!plugin.getSettings().doSaveEmptyDeathItems() && items.isEmpty())) {
final SaveOnDeathSettings settings = plugin.getSettings().getSynchronization().getSaveOnDeath();
if (plugin.isDisabling() || !settings.isEnabled() || plugin.isLocked(user.getUuid())
|| user.isNpc() || (!settings.isSaveEmptyItems() && items.isEmpty())) {
return;
}

View File

@@ -20,6 +20,7 @@
package net.william278.husksync.redis;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
@@ -62,25 +63,28 @@ public class RedisManager extends JedisPubSub {
*/
@Blocking
public void initialize() throws IllegalStateException {
final String password = plugin.getSettings().getRedisPassword();
final String host = plugin.getSettings().getRedisHost();
final int port = plugin.getSettings().getRedisPort();
final boolean useSSL = plugin.getSettings().redisUseSsl();
final Settings.RedisSettings.RedisCredentials credentials = plugin.getSettings().getRedis().getCredentials();
final String password = credentials.getPassword();
final String host = credentials.getHost();
final int port = credentials.getPort();
final boolean useSSL = credentials.isUseSsl();
// Create the jedis pool
final JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(0);
config.setTestOnBorrow(true);
config.setTestOnReturn(true);
Set<String> redisSentinelNodes = new HashSet<>(plugin.getSettings().getRedisSentinelNodes());
final Settings.RedisSettings.RedisSentinel sentinel = plugin.getSettings().getRedis().getSentinel();
Set<String> redisSentinelNodes = new HashSet<>(sentinel.getNodes());
if (redisSentinelNodes.isEmpty()) {
this.jedisPool = password.isEmpty()
? new JedisPool(config, host, port, 0, useSSL)
: new JedisPool(config, host, port, 0, password, useSSL);
} else {
String sentinelPassword = plugin.getSettings().getRedisSentinelPassword();
String redisSentinelMaster = plugin.getSettings().getRedisSentinelMaster();
this.jedisPool = new JedisSentinelPool(redisSentinelMaster, redisSentinelNodes, password.isEmpty() ? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
final String sentinelPassword = sentinel.getPassword();
this.jedisPool = new JedisSentinelPool(sentinel.getMaster(), redisSentinelNodes, password.isEmpty()
? null : password, sentinelPassword.isEmpty() ? null : sentinelPassword);
}
// Ping the server to check the connection
@@ -219,8 +223,9 @@ public class RedisManager extends JedisPubSub {
);
redisMessage.dispatch(plugin, RedisMessage.Type.REQUEST_USER_DATA);
});
return future.orTimeout(
plugin.getSettings().getNetworkLatencyMilliseconds(),
return future
.orTimeout(
plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds(),
TimeUnit.MILLISECONDS
)
.exceptionally(throwable -> {

View File

@@ -90,7 +90,8 @@ public abstract class DataSyncer {
// Calculates the max attempts the system should listen for user data for based on the latency value
private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + (
(Math.max(100, plugin.getSettings().getNetworkLatencyMilliseconds()) / 1000) * 20 / LISTEN_DELAY
(Math.max(100, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds()) / 1000)
* 20 / LISTEN_DELAY
);
}

View File

@@ -53,7 +53,7 @@ public class DelayDataSyncer extends DataSyncer {
}).orElse(false)
);
},
Math.max(0, plugin.getSettings().getNetworkLatencyMilliseconds() / 50L)
Math.max(0, plugin.getSettings().getSynchronization().getNetworkLatencyMilliseconds() / 50L)
);
}

View File

@@ -147,7 +147,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
*/
public void completeSync(boolean succeeded, @NotNull DataSnapshot.UpdateCause cause, @NotNull HuskSync plugin) {
if (succeeded) {
switch (plugin.getSettings().getNotificationDisplaySlot()) {
switch (plugin.getSettings().getSynchronization().getNotificationDisplaySlot()) {
case CHAT -> cause.getCompletedLocale(plugin).ifPresent(this::sendMessage);
case ACTION_BAR -> cause.getCompletedLocale(plugin).ifPresent(this::sendActionBar);
case TOAST -> cause.getCompletedLocale(plugin)