mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-20 15:29:19 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
480796fbee | ||
|
|
9b186ec97a | ||
|
|
d828631dea | ||
|
|
5b8de7967b | ||
|
|
4fddbc2b32 | ||
|
|
43cd367ca3 | ||
|
|
19ca504bab | ||
|
|
394b8ff1d1 | ||
|
|
4577da3336 | ||
|
|
c3b339b3dd | ||
|
|
2b91154ca2 | ||
|
|
2351be31e3 | ||
|
|
624543b93d | ||
|
|
c13f4b2a05 | ||
|
|
00b8d335d8 | ||
|
|
89d8b79ae3 | ||
|
|
acd97a1cb0 | ||
|
|
8c0f7a295f | ||
|
|
7536bfaaf5 | ||
|
|
cbf5d9c24e | ||
|
|
b9e474d946 | ||
|
|
3d232f97fb | ||
|
|
6d649d0889 | ||
|
|
0754837820 | ||
|
|
8f44dbb296 | ||
|
|
049cd8ecca | ||
|
|
2ed7705903 | ||
|
|
6bc6749e38 | ||
|
|
97a02b7a05 | ||
|
|
abc41a0aca | ||
|
|
31a14b2de7 | ||
|
|
59a0002c16 | ||
|
|
61020e04d9 | ||
|
|
ff1ace8342 | ||
|
|
847790c514 | ||
|
|
390a77b407 | ||
|
|
04ab9d14f8 | ||
|
|
3a32d481c4 | ||
|
|
8edbc029f8 | ||
|
|
258356e45d | ||
|
|
e1628b6448 | ||
|
|
fa32e97564 | ||
|
|
8080d57645 | ||
|
|
0a2f7b6cd4 | ||
|
|
26a2366876 | ||
|
|
2690ab3144 | ||
|
|
18b96944e9 | ||
|
|
3282f5739c | ||
|
|
50e66be0c0 | ||
|
|
593c88c8ba | ||
|
|
2f700b2d93 | ||
|
|
d1c95030f0 | ||
|
|
1ed2414241 | ||
|
|
8847483ff8 | ||
|
|
31552f85e4 | ||
|
|
125f142cf5 | ||
|
|
dc3882e47e | ||
|
|
dafbcad10e | ||
|
|
d1085ca7bd |
15
README.md
15
README.md
@@ -1,12 +1,15 @@
|
||||
# [](https://github.com/WiIIiam278/HuskSync)
|
||||

|
||||
[](https://discord.gg/tVYhJfyDWG)
|
||||
[](https://github.com/WiIIiam278/HuskSync/actions/workflows/java_ci.yml)
|
||||
[](https://jitpack.io/#net.william278/HuskSync)
|
||||
[](https://discord.gg/tVYhJfyDWG)
|
||||
|
||||
[Documentation, Guides & API](https://william278.net/docs/husksync/Home) · [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) · [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
[Documentation, Guides & API](https://william278.net/docs/husksync) · [Resource Page](https://www.spigotmc.org/resources/husksync.97144/) · [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
|
||||
**HuskSync** is a modern, cross-server player data synchronisation system that enables the comprehensive synchronisation of your user's data across multiple proxied servers. It does this by making use of Redis and MySQL to optimally cache data while players change servers.
|
||||
|
||||
## Features
|
||||

|
||||
|
||||
- Synchronise inventories, ender chests, advancements, statistics, experience points, health, max health, hunger, saturation, potion effects, persistent data container tags, game mode, location and more across multiple proxied servers.
|
||||
- Create and manage "snapshot" backups of user data and roll back users to previous states on-the-fly. (`/userdata`)
|
||||
- Preview, list, delete, restore & pin user data snapshots in-game with an intuitive menu.
|
||||
@@ -15,7 +18,7 @@
|
||||
- Supports segregating synchronisation across multiple distinct clusters on one network.
|
||||
|
||||
## Requirements
|
||||
* A MySQL Database (v8.0+)
|
||||
* A MySQL Database (v8.0+).
|
||||
* A Redis Database (v5.0+)
|
||||
* Any number of proxied Spigot servers (Minecraft v1.16.5+)
|
||||
|
||||
@@ -23,7 +26,7 @@
|
||||
1. Place the plugin jar file in the `/plugins/` directory of each Spigot server. You do not need to install HuskSync as a proxy plugin.
|
||||
2. Start, then stop every server to let HuskSync generate the config file.
|
||||
3. Navigate to the HuskSync config file on each server (`~/plugins/HuskSync/config.yml`) and fill in both the MySQL and Redis database credentials.
|
||||
4. Start every server again and synchronistaion will begin.
|
||||
4. Start every server again and synchronization will begin.
|
||||
|
||||
## Building
|
||||
To build HuskSync, simply run the following in the root of the repository:
|
||||
@@ -52,7 +55,7 @@ This plugin uses bStats to provide me with metrics about its usage:
|
||||
You can turn metric collection off by navigating to `~/plugins/bStats/config.yml` and editing the config to disable plugin metrics.
|
||||
|
||||
## Links
|
||||
- [Documentation, Guides & API](https://william278.net/docs/husksync/Home)
|
||||
- [Documentation, Guides & API](https://william278.net/docs/husksync)
|
||||
- [Resource Page](https://www.spigotmc.org/resources/husksync.97144/)
|
||||
- [Bug Reports](https://github.com/WiIIiam278/HuskSync/issues)
|
||||
- [Discord Support](https://discord.gg/tVYhJfyDWG) (Proof of purchase required)
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
dependencies {
|
||||
implementation project(path: ':bukkit')
|
||||
compileOnly project(path: ':common')
|
||||
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'
|
||||
relocate 'net.william278.hslmigrator', 'net.william278.husksync.libraries.hslconverter'
|
||||
}
|
||||
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
}
|
||||
18
build.gradle
18
build.gradle
@@ -6,10 +6,14 @@ plugins {
|
||||
}
|
||||
|
||||
group 'net.william278'
|
||||
version "$ext.plugin_version+${versionMetadata()}"
|
||||
version "$ext.plugin_version-${versionMetadata()}"
|
||||
|
||||
ext {
|
||||
set 'version', version.toString()
|
||||
set 'jedis_version', jedis_version.toString()
|
||||
set 'mysql_driver_version', mysql_driver_version.toString()
|
||||
set 'snappy_version', snappy_version.toString()
|
||||
set 'commons_text_version', commons_text_version.toString()
|
||||
}
|
||||
|
||||
import org.apache.tools.ant.filters.ReplaceTokens
|
||||
@@ -30,12 +34,14 @@ allprojects {
|
||||
maven { url 'https://hub.spigotmc.org/nexus/content/repositories/snapshots/' }
|
||||
maven { url 'https://repo.minebench.de/' }
|
||||
maven { url 'https://repo.alessiodp.com/releases/' }
|
||||
maven { url 'https://repo.mattstudios.me/artifactory/public/' }
|
||||
maven { url 'https://jitpack.io' }
|
||||
maven { url 'https://libraries.minecraft.net/' }
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -52,14 +58,14 @@ subprojects {
|
||||
version rootProject.version
|
||||
archivesBaseName = "${rootProject.name}-${project.name.capitalize()}"
|
||||
|
||||
if (['bukkit', 'api', 'plugin'].contains(project.name)) {
|
||||
if (['bukkit', 'plugin'].contains(project.name)) {
|
||||
shadowJar {
|
||||
destinationDirectory.set(file("$rootDir/target"))
|
||||
archiveClassifier.set('')
|
||||
}
|
||||
|
||||
// API publishing
|
||||
if ('api'.contains(project.name)) {
|
||||
if ('bukkit'.contains(project.name)) {
|
||||
java {
|
||||
withSourcesJar()
|
||||
withJavadocJar()
|
||||
@@ -98,5 +104,5 @@ def versionMetadata() {
|
||||
if (grgit == null) {
|
||||
return System.getenv("GITHUB_RUN_NUMBER") ? 'build.' + System.getenv("GITHUB_RUN_NUMBER") : 'unknown'
|
||||
}
|
||||
return 'rev.' + grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
return grgit.head().abbreviatedId + (grgit.status().clean ? '' : '-indev')
|
||||
}
|
||||
@@ -3,27 +3,40 @@ dependencies {
|
||||
implementation 'org.bstats:bstats-bukkit:3.0.0'
|
||||
implementation 'net.william278:mpdbdataconverter:1.0.1'
|
||||
implementation 'net.william278:hsldataconverter:1.0'
|
||||
implementation 'me.lucko:commodore:2.2'
|
||||
implementation 'net.kyori:adventure-platform-bukkit:4.1.2'
|
||||
implementation 'dev.triumphteam:triumph-gui:3.1.3'
|
||||
|
||||
compileOnly 'redis.clients:jedis:4.2.3'
|
||||
compileOnly 'commons-io:commons-io:2.11.0'
|
||||
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
|
||||
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
|
||||
compileOnly 'commons-io:commons-io:2.11.0'
|
||||
compileOnly 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
|
||||
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 {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
dependencies {
|
||||
exclude(dependency('com.mojang:brigadier'))
|
||||
}
|
||||
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.kyori', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
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 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
|
||||
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
|
||||
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
|
||||
relocate 'dev.triumphteam.gui', 'net.william278.husksync.libraries.triumphgui'
|
||||
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,11 +1,8 @@
|
||||
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;
|
||||
import net.william278.husksync.command.Permission;
|
||||
@@ -16,7 +13,6 @@ import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.data.JsonDataAdapter;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.database.MySqlDatabase;
|
||||
import net.william278.husksync.editor.DataEditor;
|
||||
import net.william278.husksync.event.BukkitEventCannon;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.hook.PlanHook;
|
||||
@@ -28,7 +24,10 @@ import net.william278.husksync.migrator.MpdbMigrator;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.*;
|
||||
import net.william278.husksync.util.BukkitLogger;
|
||||
import net.william278.husksync.util.BukkitResourceReader;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import org.bstats.bukkit.Metrics;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
@@ -40,6 +39,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;
|
||||
@@ -58,11 +58,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
private ResourceReader resourceReader;
|
||||
private EventListener eventListener;
|
||||
private DataAdapter dataAdapter;
|
||||
private DataEditor dataEditor;
|
||||
private EventCannon eventCannon;
|
||||
private Settings settings;
|
||||
private Locales locales;
|
||||
private List<Migrator> availableMigrators;
|
||||
|
||||
private BukkitAudiences audiences;
|
||||
private static BukkitHuskSync instance;
|
||||
|
||||
/**
|
||||
@@ -88,18 +89,21 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
this.logger = new BukkitLogger(this.getLogger());
|
||||
this.resourceReader = new BukkitResourceReader(this);
|
||||
|
||||
// Create adventure audience
|
||||
this.audiences = BukkitAudiences.create(this);
|
||||
|
||||
// Load settings and locales
|
||||
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();
|
||||
@@ -108,9 +112,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
// Prepare event cannon
|
||||
eventCannon = new BukkitEventCannon();
|
||||
|
||||
// Prepare data editor
|
||||
dataEditor = new DataEditor(locales);
|
||||
|
||||
// Prepare migrators
|
||||
availableMigrators = new ArrayList<>();
|
||||
availableMigrators.add(new LegacyMigrator(this));
|
||||
@@ -179,12 +180,27 @@ 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...");
|
||||
CompletableFuture.runAsync(() -> new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole());
|
||||
getLatestVersionIfOutdated().thenAccept(newestVersion ->
|
||||
newestVersion.ifPresent(newVersion -> getLoggingAdapter().log(Level.WARNING,
|
||||
"An update is available for HuskSync, v" + newVersion
|
||||
+ " (Currently running v" + getPluginVersion() + ")")));
|
||||
}
|
||||
} catch (HuskSyncInitializationException exception) {
|
||||
getLoggingAdapter().log(Level.SEVERE, exception.getMessage());
|
||||
getLoggingAdapter().log(Level.SEVERE, """
|
||||
***************************************************
|
||||
|
||||
Failed to initialize HuskSync!
|
||||
|
||||
***************************************************
|
||||
The plugin was disabled due to an error. Please check
|
||||
the logs below for details.
|
||||
No user data will be synchronised.
|
||||
***************************************************
|
||||
Caused by: %error_message%
|
||||
"""
|
||||
.replaceAll("%error_message%", exception.getMessage()));
|
||||
initialized.set(false);
|
||||
} catch (Exception exception) {
|
||||
getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
|
||||
@@ -237,11 +253,6 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
return dataAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull DataEditor getDataEditor() {
|
||||
return dataEditor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull EventCannon getEventCannon() {
|
||||
return eventCannon;
|
||||
@@ -268,25 +279,49 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
||||
return logger;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public @NotNull Version getPluginVersion() {
|
||||
return Version.pluginVersion(getDescription().getVersion());
|
||||
public ResourceReader getResourceReader() {
|
||||
return resourceReader;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public @NotNull Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||
public Version getPluginVersion() {
|
||||
return Version.fromString(getDescription().getVersion(), "-");
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.fromMinecraftVersionString(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the adventure Bukkit audiences
|
||||
*
|
||||
* @return The adventure Bukkit audiences
|
||||
*/
|
||||
@NotNull
|
||||
public BukkitAudiences getAudiences() {
|
||||
return audiences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Boolean> reload() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
this.settings = Settings.load(YamlDocument.create(new File(getDataFolder(), "config.yml"), Objects.requireNonNull(resourceReader.getResource("config.yml")), GeneralSettings.builder().setUseDefaults(false).build(), LoaderSettings.builder().setAutoUpdate(true).build(), DumperSettings.builder().setEncoding(DumperSettings.Encoding.UNICODE).build(), UpdaterSettings.builder().setVersioning(new BasicVersioning("config_version")).build()));
|
||||
// Load plugin settings
|
||||
this.settings = Annotaml.create(new File(getDataFolder(), "config.yml"), new Settings()).get();
|
||||
|
||||
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 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;
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(inventoryContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getInventoryData().serializedItems = serializedInventory;
|
||||
data.getInventory().orElse(ItemData.empty()).serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
@@ -95,7 +95,7 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(userData ->
|
||||
userData.ifPresent(data -> serializeItemStackArray(enderChestContents)
|
||||
.thenAccept(serializedInventory -> {
|
||||
data.getEnderChestData().serializedItems = serializedInventory;
|
||||
data.getEnderChest().orElse(ItemData.empty()).serializedItems = serializedInventory;
|
||||
setUserData(user, data).join();
|
||||
}))));
|
||||
}
|
||||
@@ -106,12 +106,14 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
* @param user the {@link User} to get the {@link BukkitInventoryMap} for
|
||||
* @return future returning the {@link BukkitInventoryMap} for the given {@link User} if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote If the {@link UserData} does not contain an inventory (i.e. inventory synchronisation is disabled), the
|
||||
* returned {@link BukkitInventoryMap} will be equivalent an empty inventory.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<BukkitInventoryMap>> getPlayerInventory(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeInventory(userData
|
||||
.getInventoryData().serializedItems).join()));
|
||||
.map(userData -> deserializeInventory(userData.getInventory()
|
||||
.orElse(ItemData.empty()).serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,12 +122,14 @@ public class HuskSyncAPI extends BaseHuskSyncAPI {
|
||||
* @param user the {@link User} to get the Ender Chest contents of
|
||||
* @return future returning the {@link ItemStack} array of Ender Chest items for the user if they exist,
|
||||
* otherwise an empty {@link Optional}
|
||||
* @apiNote If the {@link UserData} does not contain an Ender Chest (i.e. Ender Chest synchronisation is disabled),
|
||||
* the returned {@link BukkitInventoryMap} will be equivalent to an empty inventory.
|
||||
* @since 2.0
|
||||
*/
|
||||
public CompletableFuture<Optional<ItemStack[]>> getPlayerEnderChest(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> getUserData(user).join()
|
||||
.map(userData -> deserializeItemStackArray(userData
|
||||
.getEnderChestData().serializedItems).join()));
|
||||
.map(userData -> deserializeItemStackArray(userData.getEnderChest()
|
||||
.orElse(ItemData.empty()).serializedItems).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import me.lucko.commodore.file.CommodoreFileReader;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.command.PluginCommand;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Used for registering Brigadier hooks on platforms that support commodore for rich command syntax
|
||||
*/
|
||||
public class BrigadierUtil {
|
||||
|
||||
protected static void registerCommodore(@NotNull BukkitHuskSync plugin, @NotNull PluginCommand pluginCommand,
|
||||
@NotNull CommandBase command) {
|
||||
// Register command descriptions via commodore (brigadier wrapper)
|
||||
try (InputStream pluginFile = plugin.getResourceReader()
|
||||
.getResource("commodore/" + command.command + ".commodore")) {
|
||||
CommodoreProvider.getCommodore(plugin).register(pluginCommand,
|
||||
CommodoreFileReader.INSTANCE.parse(pluginFile),
|
||||
player -> player.hasPermission(command.permission));
|
||||
} catch (IOException e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"Failed to load " + command.command + ".commodore command definitions", e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import me.lucko.commodore.CommodoreProvider;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import org.bukkit.command.*;
|
||||
import org.bukkit.entity.Player;
|
||||
@@ -18,14 +19,14 @@ public class BukkitCommand implements CommandExecutor, TabExecutor {
|
||||
/**
|
||||
* The {@link CommandBase} that will be executed
|
||||
*/
|
||||
private final CommandBase command;
|
||||
protected final CommandBase command;
|
||||
|
||||
/**
|
||||
* The implementing plugin
|
||||
*/
|
||||
private final HuskSync plugin;
|
||||
private final BukkitHuskSync plugin;
|
||||
|
||||
public BukkitCommand(@NotNull CommandBase command, @NotNull HuskSync implementor) {
|
||||
public BukkitCommand(@NotNull CommandBase command, @NotNull BukkitHuskSync implementor) {
|
||||
this.command = command;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
@@ -40,6 +41,9 @@ public class BukkitCommand implements CommandExecutor, TabExecutor {
|
||||
pluginCommand.setTabCompleter(this);
|
||||
pluginCommand.setPermission(command.permission);
|
||||
pluginCommand.setDescription(command.getDescription());
|
||||
if (CommodoreProvider.isSupported()) {
|
||||
BrigadierUtil.registerCommodore(plugin, pluginCommand, command);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -51,8 +55,9 @@ public class BukkitCommand implements CommandExecutor, TabExecutor {
|
||||
if (this.command instanceof ConsoleExecutable consoleExecutable) {
|
||||
consoleExecutable.onConsoleExecute(args);
|
||||
} else {
|
||||
plugin.getLocales().getLocale("error_in_game_command_only").
|
||||
ifPresent(locale -> sender.spigot().sendMessage(locale.toComponent()));
|
||||
plugin.getLocales().getLocale("error_in_game_command_only")
|
||||
.ifPresent(locale -> plugin.getAudiences().sender(sender)
|
||||
.sendMessage(locale.toComponent()));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -11,6 +11,8 @@ import java.util.Optional;
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitInventoryMap {
|
||||
|
||||
public static final int INVENTORY_SLOT_COUNT = 41;
|
||||
|
||||
private ItemStack[] contents;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
import org.bukkit.util.io.BukkitObjectInputStream;
|
||||
@@ -13,6 +14,7 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BukkitSerializer {
|
||||
|
||||
@@ -45,6 +47,7 @@ public class BukkitSerializer {
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to serialize item stack data", e);
|
||||
throw new DataSerializationException("Failed to serialize item stack data", e);
|
||||
}
|
||||
});
|
||||
@@ -95,6 +98,7 @@ public class BukkitSerializer {
|
||||
return inventoryContents;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to deserialize item stack data", e);
|
||||
throw new DataSerializationException("Failed to deserialize item stack data", e);
|
||||
}
|
||||
});
|
||||
@@ -151,6 +155,7 @@ public class BukkitSerializer {
|
||||
// Return encoded data, using the encoder from SnakeYaml to get a ByteArray conversion
|
||||
return Base64Coder.encodeLines(byteOutputStream.toByteArray());
|
||||
} catch (IOException e) {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to serialize potion effect data", e);
|
||||
throw new DataSerializationException("Failed to serialize potion effect data", e);
|
||||
}
|
||||
});
|
||||
@@ -186,6 +191,7 @@ public class BukkitSerializer {
|
||||
return potionEffects;
|
||||
}
|
||||
} catch (IOException | ClassNotFoundException e) {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.SEVERE, "Failed to deserialize potion effect data", e);
|
||||
throw new DataSerializationException("Failed to deserialize potion effects", e);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private boolean cancelled = false;
|
||||
@@ -57,4 +58,8 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,16 @@ package net.william278.husksync.event;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.event.Event;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected BukkitEvent() {
|
||||
}
|
||||
|
||||
@@ -26,4 +31,14 @@ public abstract class BukkitEvent extends Event implements net.william278.husksy
|
||||
return eventFireFuture;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@ import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
|
||||
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
protected final Player player;
|
||||
|
||||
protected BukkitPlayerEvent(@NotNull Player player) {
|
||||
@@ -32,4 +36,15 @@ public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEve
|
||||
return eventFireFuture;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import org.bukkit.event.Cancellable;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
private boolean cancelled = false;
|
||||
@@ -41,4 +42,8 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.HandlerList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
|
||||
private static final HandlerList HANDLER_LIST = new HandlerList();
|
||||
|
||||
@@ -16,4 +17,8 @@ public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCo
|
||||
public HandlerList getHandlers() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return HANDLER_LIST;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package net.william278.husksync.listener;
|
||||
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import net.william278.husksync.data.BukkitInventoryMap;
|
||||
import net.william278.husksync.data.BukkitSerializer;
|
||||
import net.william278.husksync.data.DataSerializationException;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.editor.ItemEditorMenuType;
|
||||
import net.william278.husksync.player.BukkitPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import org.bukkit.Bukkit;
|
||||
@@ -14,21 +13,19 @@ import org.bukkit.event.EventPriority;
|
||||
import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.EntityPickupItemEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.inventory.InventoryClickEvent;
|
||||
import org.bukkit.event.inventory.InventoryCloseEvent;
|
||||
import org.bukkit.event.inventory.InventoryOpenEvent;
|
||||
import org.bukkit.event.player.PlayerDropItemEvent;
|
||||
import org.bukkit.event.player.PlayerInteractEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerQuitEvent;
|
||||
import org.bukkit.event.world.WorldSaveEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class BukkitEventListener extends EventListener implements Listener {
|
||||
@@ -50,31 +47,37 @@ public class BukkitEventListener extends EventListener implements Listener {
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onWorldSave(@NotNull WorldSaveEvent event) {
|
||||
CompletableFuture.runAsync(() -> super.handleAsyncWorldSave(event.getWorld().getPlayers().stream()
|
||||
.map(BukkitPlayer::adapt).collect(Collectors.toList())));
|
||||
// Handle saving player data snapshots when the world saves
|
||||
if (!plugin.getSettings().saveOnWorldSave) return;
|
||||
|
||||
CompletableFuture.runAsync(() -> super.saveOnWorldSave(event.getWorld().getPlayers()
|
||||
.stream().map(BukkitPlayer::adapt)
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onInventoryClose(@NotNull InventoryCloseEvent event) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
final OnlineUser user = BukkitPlayer.adapt(player);
|
||||
plugin.getDataEditor().getEditingInventoryData(user).ifPresent(menu -> {
|
||||
try {
|
||||
BukkitSerializer.serializeItemStackArray(Arrays.copyOf(event.getInventory().getContents(),
|
||||
menu.itemEditorMenuType == ItemEditorMenuType.INVENTORY_VIEWER
|
||||
? player.getInventory().getSize()
|
||||
: player.getEnderChest().getSize())).thenAccept(
|
||||
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
|
||||
} catch (DataSerializationException e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"Failed to serialize inventory data during menu close", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
final OnlineUser user = BukkitPlayer.adapt(event.getEntity());
|
||||
|
||||
// If the player is locked or the plugin disabling, clear their drops
|
||||
if (cancelPlayerEvent(user)) {
|
||||
event.getDrops().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle saving player data snapshots on death
|
||||
if (!plugin.getSettings().saveOnDeath) return;
|
||||
|
||||
// Truncate the drops list to the inventory size and save the player's inventory
|
||||
final int maxInventorySize = BukkitInventoryMap.INVENTORY_SLOT_COUNT;
|
||||
if (event.getDrops().size() > maxInventorySize) {
|
||||
event.getDrops().subList(maxInventorySize, event.getDrops().size()).clear();
|
||||
}
|
||||
BukkitSerializer.serializeItemStackArray(event.getDrops().toArray(new ItemStack[0]))
|
||||
.thenAccept(serializedDrops -> super.saveOnPlayerDeath(user, new ItemData(serializedDrops)));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Events to cancel if the player has not been set yet
|
||||
*/
|
||||
@@ -107,13 +110,6 @@ public class BukkitEventListener extends EventListener implements Listener {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(event.getPlayer())));
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryClick(@NotNull InventoryClickEvent event) {
|
||||
if (event.getWhoClicked() instanceof Player player) {
|
||||
event.setCancelled(cancelInventoryClick(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onInventoryOpen(@NotNull InventoryOpenEvent event) {
|
||||
if (event.getPlayer() instanceof Player player) {
|
||||
@@ -121,10 +117,10 @@ public class BukkitEventListener extends EventListener implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) {
|
||||
event.getDrops().clear();
|
||||
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
|
||||
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
|
||||
if (event.getEntity() instanceof Player player) {
|
||||
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -18,6 +17,7 @@ import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -37,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();
|
||||
@@ -68,14 +68,15 @@ public class LegacyMigrator extends Migrator {
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
|
||||
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the legacy database...");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the legacy database (this might take a while)...");
|
||||
final List<LegacyData> dataToMigrate = new ArrayList<>();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
SELECT `uuid`, `username`, `inventory`, `ender_chest`, `health`, `max_health`, `health_scale`, `hunger`, `saturation`, `saturation_exhaustion`, `selected_slot`, `status_effects`, `total_experience`, `exp_level`, `exp_progress`, `game_mode`, `statistics`, `is_flying`, `advancements`, `location`
|
||||
FROM `%source_players_table%`
|
||||
INNER JOIN `%source_data_table%`
|
||||
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`;
|
||||
ON `%source_players_table%`.`id` = `%source_data_table%`.`player_id`
|
||||
WHERE `username` IS NOT NULL;
|
||||
""".replaceAll(Pattern.quote("%source_players_table%"), sourcePlayersTable)
|
||||
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
|
||||
try (final ResultSet resultSet = statement.executeQuery()) {
|
||||
@@ -104,7 +105,7 @@ public class LegacyMigrator extends Migrator {
|
||||
resultSet.getString("location")
|
||||
));
|
||||
playersMigrated++;
|
||||
if (playersMigrated % 25 == 0) {
|
||||
if (playersMigrated % 50 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
|
||||
}
|
||||
}
|
||||
@@ -112,14 +113,22 @@ public class LegacyMigrator extends Migrator {
|
||||
}
|
||||
}
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the legacy database!");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the latest HuskSync user data format...");
|
||||
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData ->
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
}))));
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> {
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
})).join();
|
||||
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converted legacy data for " + playersConverted + " players...");
|
||||
}
|
||||
}).join());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
@@ -277,13 +286,16 @@ public class LegacyMigrator extends Migrator {
|
||||
legacyLocationData == null ? 90f : legacyLocationData.yaw(),
|
||||
legacyLocationData == null ? 180f : legacyLocationData.pitch());
|
||||
|
||||
return new UserData(new StatusData(health, maxHealth, healthScale, hunger, saturation,
|
||||
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying),
|
||||
new ItemData(serializedInventory), new ItemData(serializedEnderChest),
|
||||
new PotionEffectData(serializedPotionEffects), convertedAdvancements,
|
||||
convertedStatisticData, convertedLocationData,
|
||||
new PersistentDataContainerData(new HashMap<>()),
|
||||
minecraftVersion);
|
||||
return UserData.builder(minecraftVersion)
|
||||
.setStatus(new StatusData(health, maxHealth, healthScale, hunger, saturation,
|
||||
saturationExhaustion, selectedSlot, totalExp, expLevel, expProgress, gameMode, isFlying))
|
||||
.setInventory(new ItemData(serializedInventory))
|
||||
.setEnderChest(new ItemData(serializedEnderChest))
|
||||
.setPotionEffects(new PotionEffectData(serializedPotionEffects))
|
||||
.setAdvancements(convertedAdvancements)
|
||||
.setStatistics(convertedStatisticData)
|
||||
.setLocation(convertedLocationData)
|
||||
.build();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
@@ -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,8 +15,11 @@ 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;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -40,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";
|
||||
@@ -73,7 +75,7 @@ public class MpdbMigrator extends Migrator {
|
||||
connectionPool.setPassword(sourcePassword);
|
||||
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase());
|
||||
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database...");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Downloading raw data from the MySQLPlayerDataBridge database (this might take a while)...");
|
||||
final List<MpdbData> dataToMigrate = new ArrayList<>();
|
||||
try (final Connection connection = connectionPool.getConnection()) {
|
||||
try (final PreparedStatement statement = connection.prepareStatement("""
|
||||
@@ -108,14 +110,21 @@ public class MpdbMigrator extends Migrator {
|
||||
}
|
||||
}
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data...");
|
||||
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData ->
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
})));
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
|
||||
|
||||
final AtomicInteger playersConverted = new AtomicInteger();
|
||||
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> {
|
||||
plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
|
||||
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
|
||||
.exceptionally(exception -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
|
||||
return null;
|
||||
}).join();
|
||||
playersConverted.getAndIncrement();
|
||||
if (playersConverted.get() % 50 == 0) {
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Converted MySQLPlayerDataBridge data for " + playersConverted + " players...");
|
||||
}
|
||||
}).join());
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
@@ -272,18 +281,14 @@ public class MpdbMigrator extends Migrator {
|
||||
}
|
||||
|
||||
// Create user data record
|
||||
return new UserData(new StatusData(20, 20, 0, 20, 10,
|
||||
1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
|
||||
false),
|
||||
new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()),
|
||||
new ItemData(BukkitSerializer.serializeItemStackArray(converter
|
||||
.getItemStackFromSerializedData(serializedEnderChest)).join()),
|
||||
new PotionEffectData(""), new ArrayList<>(),
|
||||
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()),
|
||||
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
|
||||
0f, 0f),
|
||||
new PersistentDataContainerData(new HashMap<>()),
|
||||
minecraftVersion);
|
||||
return UserData.builder(minecraftVersion)
|
||||
.setStatus(new StatusData(20, 20, 0, 20, 10,
|
||||
1, 0, totalExp, expLevel, expProgress, "SURVIVAL",
|
||||
false))
|
||||
.setInventory(new ItemData(BukkitSerializer.serializeItemStackArray(inventory.getContents()).join()))
|
||||
.setEnderChest(new ItemData(BukkitSerializer.serializeItemStackArray(converter
|
||||
.getItemStackFromSerializedData(serializedEnderChest)).join()))
|
||||
.build();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.ChatMessageType;
|
||||
import net.md_5.bungee.api.chat.BaseComponent;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDownParser;
|
||||
import dev.triumphteam.gui.builder.gui.StorageBuilder;
|
||||
import dev.triumphteam.gui.guis.Gui;
|
||||
import dev.triumphteam.gui.guis.StorageGui;
|
||||
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 net.william278.husksync.util.Version;
|
||||
import org.apache.commons.lang.ArrayUtils;
|
||||
import org.bukkit.*;
|
||||
import org.bukkit.advancement.Advancement;
|
||||
import org.bukkit.advancement.AdvancementProgress;
|
||||
@@ -15,7 +17,7 @@ import org.bukkit.attribute.Attribute;
|
||||
import org.bukkit.entity.EntityType;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.event.player.PlayerTeleportEvent;
|
||||
import org.bukkit.inventory.Inventory;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.persistence.PersistentDataContainer;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.potion.PotionEffect;
|
||||
@@ -33,11 +35,27 @@ import java.util.logging.Level;
|
||||
*/
|
||||
public class BukkitPlayer extends OnlineUser {
|
||||
|
||||
private static final PersistentDataType<?, ?>[] PRIMITIVE_PERSISTENT_DATA_TYPES = new PersistentDataType<?, ?>[]{
|
||||
PersistentDataType.BYTE,
|
||||
PersistentDataType.SHORT,
|
||||
PersistentDataType.INTEGER,
|
||||
PersistentDataType.LONG,
|
||||
PersistentDataType.FLOAT,
|
||||
PersistentDataType.DOUBLE,
|
||||
PersistentDataType.STRING,
|
||||
PersistentDataType.BYTE_ARRAY,
|
||||
PersistentDataType.INTEGER_ARRAY,
|
||||
PersistentDataType.LONG_ARRAY,
|
||||
PersistentDataType.TAG_CONTAINER_ARRAY,
|
||||
PersistentDataType.TAG_CONTAINER};
|
||||
|
||||
private final Player player;
|
||||
private final Audience audience;
|
||||
|
||||
private BukkitPlayer(@NotNull Player player) {
|
||||
super(player.getUniqueId(), player.getName());
|
||||
this.player = player;
|
||||
this.audience = BukkitHuskSync.getInstance().getAudiences().player(player);
|
||||
}
|
||||
|
||||
public static BukkitPlayer adapt(@NotNull Player player) {
|
||||
@@ -68,23 +86,22 @@ 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;
|
||||
if (healthToSet <= 0) {
|
||||
if (healthToSet < 1) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet));
|
||||
} else {
|
||||
player.setHealth(healthToSet);
|
||||
@@ -98,24 +115,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);
|
||||
@@ -402,17 +419,61 @@ public class BukkitPlayer extends OnlineUser {
|
||||
if (container.isEmpty()) {
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
}
|
||||
final HashMap<String, Byte[]> persistentDataMap = new HashMap<>();
|
||||
// Set persistent data keys; ignore keys that we cannot synchronise as byte arrays
|
||||
final HashMap<String, PersistentDataTag<?>> persistentDataMap = new HashMap<>();
|
||||
for (final NamespacedKey key : container.getKeys()) {
|
||||
try {
|
||||
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY)));
|
||||
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||
PersistentDataType<?, ?> type = null;
|
||||
for (PersistentDataType<?, ?> dataType : PRIMITIVE_PERSISTENT_DATA_TYPES) {
|
||||
if (container.has(key, dataType)) {
|
||||
type = dataType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (type != null) {
|
||||
// This is absolutely disgusting code and needs to be swiftly put out of its misery with a refactor
|
||||
final Class<?> primitiveType = type.getPrimitiveType();
|
||||
if (String.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.STRING,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.STRING))));
|
||||
} else if (int.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.INTEGER))));
|
||||
} else if (double.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.DOUBLE,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.DOUBLE))));
|
||||
} else if (float.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.FLOAT,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.FLOAT))));
|
||||
} else if (long.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.LONG,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.LONG))));
|
||||
} else if (short.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.SHORT,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.SHORT))));
|
||||
} else if (byte.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.BYTE))));
|
||||
} else if (byte[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.BYTE_ARRAY))));
|
||||
} else if (int[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.INTEGER_ARRAY))));
|
||||
} else if (long[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.LONG_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.LONG_ARRAY))));
|
||||
} else if (PersistentDataContainer.class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.TAG_CONTAINER,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.TAG_CONTAINER))));
|
||||
} else if (PersistentDataContainer[].class.equals(primitiveType)) {
|
||||
persistentDataMap.put(key.toString(), new PersistentDataTag<>(BukkitPersistentDataTagType.TAG_CONTAINER_ARRAY,
|
||||
Objects.requireNonNull(container.get(key, PersistentDataType.TAG_CONTAINER_ARRAY))));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new PersistentDataContainerData(persistentDataMap);
|
||||
}).exceptionally(throwable -> {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING, "Could not read " + player.getName() + "'s persistent data map, skipping!");
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
|
||||
"Could not read " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
});
|
||||
@@ -423,14 +484,64 @@ public class BukkitPlayer extends OnlineUser {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
|
||||
player.getPersistentDataContainer().remove(namespacedKey));
|
||||
persistentDataContainerData.persistentDataMap.keySet().forEach(keyString -> {
|
||||
persistentDataContainerData.getTags().forEach(keyString -> {
|
||||
final NamespacedKey key = NamespacedKey.fromString(keyString);
|
||||
if (key != null) {
|
||||
final byte[] data = ArrayUtils.toPrimitive(persistentDataContainerData
|
||||
.persistentDataMap.get(keyString));
|
||||
player.getPersistentDataContainer().set(key, PersistentDataType.BYTE_ARRAY, data);
|
||||
// Set a tag with the given key and value. This is crying out for a refactor.
|
||||
persistentDataContainerData.getTagType(keyString).ifPresentOrElse(dataType -> {
|
||||
switch (dataType) {
|
||||
case BYTE -> persistentDataContainerData.getTagValue(keyString, byte.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.BYTE, value));
|
||||
case SHORT -> persistentDataContainerData.getTagValue(keyString, short.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.SHORT, value));
|
||||
case INTEGER -> persistentDataContainerData.getTagValue(keyString, int.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.INTEGER, value));
|
||||
case LONG -> persistentDataContainerData.getTagValue(keyString, long.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.LONG, value));
|
||||
case FLOAT -> persistentDataContainerData.getTagValue(keyString, float.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.FLOAT, value));
|
||||
case DOUBLE -> persistentDataContainerData.getTagValue(keyString, double.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.DOUBLE, value));
|
||||
case STRING -> persistentDataContainerData.getTagValue(keyString, String.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.STRING, value));
|
||||
case BYTE_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, byte[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.BYTE_ARRAY, value));
|
||||
case INTEGER_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, int[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.INTEGER_ARRAY, value));
|
||||
case LONG_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, long[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.LONG_ARRAY, value));
|
||||
case TAG_CONTAINER ->
|
||||
persistentDataContainerData.getTagValue(keyString, PersistentDataContainer.class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.TAG_CONTAINER, value));
|
||||
case TAG_CONTAINER_ARRAY ->
|
||||
persistentDataContainerData.getTagValue(keyString, PersistentDataContainer[].class).ifPresent(
|
||||
value -> player.getPersistentDataContainer().set(key,
|
||||
PersistentDataType.TAG_CONTAINER_ARRAY, value));
|
||||
}
|
||||
}, () -> BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
|
||||
"Could not set " + player.getName() + "'s persistent data key " + keyString +
|
||||
" as it has an invalid type. Skipping!"));
|
||||
}
|
||||
});
|
||||
}).exceptionally(throwable -> {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING,
|
||||
"Could not write " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,7 +558,7 @@ public class BukkitPlayer extends OnlineUser {
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||
return Version.fromMinecraftVersionString(Bukkit.getBukkitVersion());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -456,23 +567,72 @@ public class BukkitPlayer extends OnlineUser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMenu(@NotNull ItemEditorMenu menu) {
|
||||
BukkitSerializer.deserializeItemStackArray(menu.itemData.serializedItems).thenAccept(inventoryContents -> {
|
||||
final Inventory inventory = Bukkit.createInventory(player, menu.itemEditorMenuType.slotCount,
|
||||
BaseComponent.toLegacyText(menu.menuTitle.toComponent()));
|
||||
inventory.setContents(inventoryContents);
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.openInventory(inventory));
|
||||
public CompletableFuture<Optional<ItemData>> showMenu(@NotNull ItemData itemData, boolean editable,
|
||||
int minimumRows, @NotNull MineDown title) {
|
||||
final CompletableFuture<Optional<ItemData>> updatedData = new CompletableFuture<>();
|
||||
|
||||
// Deserialize the item data to be shown and show it in a triumph GUI
|
||||
BukkitSerializer.deserializeItemStackArray(itemData.serializedItems).thenAccept(items -> {
|
||||
try {
|
||||
// Build the GUI and populate with items
|
||||
final int itemCount = items.length;
|
||||
final StorageBuilder guiBuilder = Gui.storage()
|
||||
.title(title.toComponent())
|
||||
.rows(Math.max(minimumRows, (int) Math.ceil(itemCount / 9.0)))
|
||||
.disableAllInteractions()
|
||||
.enableOtherActions();
|
||||
final StorageGui gui = editable ? guiBuilder.enableAllInteractions().create() : guiBuilder.create();
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
if (items[i] != null) {
|
||||
gui.getInventory().setItem(i, items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Complete the future with updated data (if editable) when the GUI is closed
|
||||
gui.setCloseGuiAction(event -> {
|
||||
if (!editable) {
|
||||
updatedData.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
// Get and save the updated items
|
||||
final ItemStack[] updatedItems = Arrays.copyOf(event.getPlayer().getOpenInventory()
|
||||
.getTopInventory().getContents().clone(), itemCount);
|
||||
BukkitSerializer.serializeItemStackArray(updatedItems).thenAccept(serializedItems -> {
|
||||
if (serializedItems.equals(itemData.serializedItems)) {
|
||||
updatedData.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
updatedData.complete(Optional.of(new ItemData(serializedItems)));
|
||||
});
|
||||
});
|
||||
|
||||
// Display the GUI (synchronously; on the main server thread)
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> gui.open(player));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
return updatedData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() < 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendActionBar(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent());
|
||||
audience.sendActionBar(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@NotNull MineDown mineDown) {
|
||||
player.spigot().sendMessage(mineDown.replace().toComponent());
|
||||
audience.sendMessage(mineDown
|
||||
.disable(MineDownParser.Option.SIMPLE_FORMATTING)
|
||||
.replace().toComponent());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.md_5.bungee.api.chat.TextComponent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.logging.Level;
|
||||
@@ -24,11 +22,6 @@ public class BukkitLogger extends Logger {
|
||||
logger.log(level, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull MineDown mineDown) {
|
||||
logger.log(level, TextComponent.toLegacyText(mineDown.toComponent()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(@NotNull String message) {
|
||||
logger.info(message);
|
||||
|
||||
@@ -3,7 +3,6 @@ package net.william278.husksync.util;
|
||||
import net.william278.husksync.BukkitHuskSync;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -20,9 +19,4 @@ public class BukkitResourceReader implements ResourceReader {
|
||||
return Objects.requireNonNull(plugin.getResource(fileName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NotNull File getDataFolder() {
|
||||
return plugin.getDataFolder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
3
bukkit/src/main/resources/commodore/enderchest.commodore
Normal file
3
bukkit/src/main/resources/commodore/enderchest.commodore
Normal file
@@ -0,0 +1,3 @@
|
||||
inventory {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
5
bukkit/src/main/resources/commodore/husksync.commodore
Normal file
5
bukkit/src/main/resources/commodore/husksync.commodore
Normal file
@@ -0,0 +1,5 @@
|
||||
husksync {
|
||||
update;
|
||||
about;
|
||||
reload;
|
||||
}
|
||||
3
bukkit/src/main/resources/commodore/inventory.commodore
Normal file
3
bukkit/src/main/resources/commodore/inventory.commodore
Normal file
@@ -0,0 +1,3 @@
|
||||
enderchest {
|
||||
name brigadier:string single_word;
|
||||
}
|
||||
35
bukkit/src/main/resources/commodore/userdata.commodore
Normal file
35
bukkit/src/main/resources/commodore/userdata.commodore
Normal file
@@ -0,0 +1,35 @@
|
||||
userdata {
|
||||
view {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
list {
|
||||
name brigadier:string single_word {
|
||||
page brigadier:integer;
|
||||
}
|
||||
}
|
||||
delete {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
restore {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
pin {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word;
|
||||
}
|
||||
}
|
||||
dump {
|
||||
name brigadier:string single_word {
|
||||
version brigadier:string single_word {
|
||||
web;
|
||||
file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,17 @@ softdepend:
|
||||
- MysqlPlayerDataBridge
|
||||
- Plan
|
||||
libraries:
|
||||
- 'mysql:mysql-connector-java:8.0.29'
|
||||
- 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
- 'dev.dejvokep:boosted-yaml:1.2'
|
||||
- 'redis.clients:jedis:${jedis_version}'
|
||||
- 'mysql:mysql-connector-java:${mysql_driver_version}'
|
||||
- 'org.xerial.snappy:snappy-java:${snappy_version}'
|
||||
- 'org.apache.commons:commons-text:${commons_text_version}'
|
||||
|
||||
commands:
|
||||
husksync:
|
||||
usage: '/husksync <update/info/reload/migrate>'
|
||||
description: 'Manage the HuskSync plugin'
|
||||
userdata:
|
||||
usage: '/userdata <view/list/delete/restore/pin> <username> [version_uuid]'
|
||||
usage: '/userdata <view/list/delete/restore/pin/dump> <username> [version_uuid]'
|
||||
description: 'View, manage & restore player userdata'
|
||||
inventory:
|
||||
usage: '/inventory <username> [version_uuid]'
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
dependencies {
|
||||
implementation 'commons-io:commons-io:2.11.0'
|
||||
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
|
||||
implementation 'de.themoep:minedown-adventure:1.7.1-SNAPSHOT'
|
||||
implementation 'net.kyori:adventure-api:4.11.0'
|
||||
implementation 'com.google.code.gson:gson:2.9.0'
|
||||
implementation('redis.clients:jedis:4.2.3') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
implementation ('com.zaxxer:HikariCP:5.0.1') {
|
||||
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') {
|
||||
exclude module: 'slf4j-api'
|
||||
}
|
||||
|
||||
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
|
||||
compileOnly 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
compileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690'
|
||||
compileOnly 'redis.clients:jedis:' + jedis_version
|
||||
compileOnly 'org.xerial.snappy:snappy-java:' + snappy_version
|
||||
compileOnly 'org.apache.commons:commons-text:' + commons_text_version
|
||||
|
||||
testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4'
|
||||
testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690'
|
||||
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3'
|
||||
testCompileOnly 'org.jetbrains:annotations:23.0.0'
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
relocate 'org.apache', 'net.william278.husksync.libraries'
|
||||
relocate 'org.apache.commons.io', 'net.william278.husksync.libraries.commons.io'
|
||||
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
|
||||
relocate 'de.themoep', 'net.william278.husksync.libraries'
|
||||
relocate 'net.kyori', 'net.william278.husksync.libraries'
|
||||
relocate 'org.jetbrains', 'net.william278.husksync.libraries'
|
||||
relocate 'org.intellij', 'net.william278.husksync.libraries'
|
||||
relocate 'com.zaxxer', 'net.william278.husksync.libraries'
|
||||
relocate 'com.google', 'net.william278.husksync.libraries'
|
||||
relocate 'redis.clients', 'net.william278.husksync.libraries'
|
||||
relocate 'org.json', 'net.william278.husksync.libraries.json'
|
||||
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,18 +1,20 @@
|
||||
package net.william278.husksync;
|
||||
|
||||
import net.william278.desertwell.UpdateChecker;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.DataAdapter;
|
||||
import net.william278.husksync.editor.DataEditor;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.redis.RedisManager;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.Version;
|
||||
import net.william278.husksync.util.ResourceReader;
|
||||
import net.william278.desertwell.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -24,6 +26,8 @@ import java.util.concurrent.CompletableFuture;
|
||||
*/
|
||||
public interface HuskSync {
|
||||
|
||||
int SPIGOT_RESOURCE_ID = 97144;
|
||||
|
||||
/**
|
||||
* Returns a set of online players.
|
||||
*
|
||||
@@ -66,14 +70,6 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
DataAdapter getDataAdapter();
|
||||
|
||||
/**
|
||||
* Returns the data editor implementation
|
||||
*
|
||||
* @return the {@link DataEditor} implementation
|
||||
*/
|
||||
@NotNull
|
||||
DataEditor getDataEditor();
|
||||
|
||||
/**
|
||||
* Returns the event firing cannon
|
||||
*
|
||||
@@ -114,6 +110,14 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Logger getLoggingAdapter();
|
||||
|
||||
/**
|
||||
* Returns the plugin resource file reader
|
||||
*
|
||||
* @return the {@link ResourceReader}
|
||||
*/
|
||||
@NotNull
|
||||
ResourceReader getResourceReader();
|
||||
|
||||
/**
|
||||
* Returns the plugin version
|
||||
*
|
||||
@@ -122,6 +126,30 @@ public interface HuskSync {
|
||||
@NotNull
|
||||
Version getPluginVersion();
|
||||
|
||||
/**
|
||||
* Returns the plugin data folder
|
||||
*
|
||||
* @return the plugin data folder as a {@link File}
|
||||
*/
|
||||
@NotNull
|
||||
File getDataFolder();
|
||||
|
||||
/**
|
||||
* Returns a future returning the latest plugin {@link Version} if the plugin is out-of-date
|
||||
*
|
||||
* @return a {@link CompletableFuture} returning the latest {@link Version} if the current one is out-of-date
|
||||
*/
|
||||
default CompletableFuture<Optional<Version>> getLatestVersionIfOutdated() {
|
||||
final UpdateChecker updateChecker = UpdateChecker.create(getPluginVersion(), SPIGOT_RESOURCE_ID);
|
||||
return updateChecker.isUpToDate().thenApply(upToDate -> {
|
||||
if (upToDate) {
|
||||
return Optional.empty();
|
||||
} else {
|
||||
return Optional.of(updateChecker.getLatestVersion().join());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Minecraft version implementation
|
||||
*
|
||||
|
||||
@@ -72,7 +72,7 @@ public abstract class BaseHuskSyncAPI {
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
if (user instanceof OnlineUser) {
|
||||
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter()).join();
|
||||
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join();
|
||||
} else {
|
||||
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||
}
|
||||
@@ -103,8 +103,9 @@ public abstract class BaseHuskSyncAPI {
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings())
|
||||
.thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,7 +52,7 @@ public abstract class CommandBase {
|
||||
*/
|
||||
public String getDescription() {
|
||||
return plugin.getLocales().getRawLocale(command + "_command_description")
|
||||
.orElse("A HuskHomes command");
|
||||
.orElse("A HuskSync command");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataBuilder;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -55,29 +55,42 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
|
||||
}
|
||||
|
||||
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||
@NotNull User dataOwner, final boolean allowEdit) {
|
||||
@NotNull User dataOwner, boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
|
||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(enderChestDataOnClose -> {
|
||||
if (!menu.canEdit) {
|
||||
return;
|
||||
}
|
||||
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
|
||||
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
|
||||
data.getStatisticsData(), data.getLocationData(),
|
||||
data.getPersistentDataContainerData(),
|
||||
plugin.getMinecraftVersion().toString());
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDERCHEST_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
data.getEnderChest().ifPresent(itemData -> {
|
||||
// Show message
|
||||
plugin.getLocales().getLocale("ender_chest_viewer_opened", dataOwner.username,
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss")
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
|
||||
// Show inventory menu
|
||||
player.showMenu(itemData, allowEdit, 3, plugin.getLocales()
|
||||
.getLocale("ender_chest_viewer_menu_title", dataOwner.username)
|
||||
.orElse(new MineDown("Ender Chest Viewer"))).thenAccept(dataOnClose -> {
|
||||
if (dataOnClose.isEmpty() || !allowEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the updated data
|
||||
final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
|
||||
data.getStatus().ifPresent(builder::setStatus);
|
||||
data.getAdvancements().ifPresent(builder::setAdvancements);
|
||||
data.getLocation().ifPresent(builder::setLocation);
|
||||
data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer);
|
||||
data.getStatistics().ifPresent(builder::setStatistics);
|
||||
data.getPotionEffects().ifPresent(builder::setPotionEffects);
|
||||
data.getInventory().ifPresent(builder::setInventory);
|
||||
builder.setEnderChest(dataOnClose.get());
|
||||
|
||||
// Set the updated data
|
||||
final UserData updatedUserData = builder.build();
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.desertwell.AboutMenu;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.migrator.Migrator;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.util.UpdateChecker;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
@@ -16,16 +16,41 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class HuskSyncCommand extends CommandBase implements TabCompletable, ConsoleExecutable {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"update", "about", "reload", "migrate"};
|
||||
private final String[] SUB_COMMANDS = {"update", "about", "reload", "migrate"};
|
||||
private final AboutMenu aboutMenu;
|
||||
|
||||
public HuskSyncCommand(@NotNull HuskSync implementor) {
|
||||
super("husksync", Permission.COMMAND_HUSKSYNC, implementor);
|
||||
this.aboutMenu = AboutMenu.create("HuskSync")
|
||||
.withDescription("A modern, cross-server player data synchronization system")
|
||||
.withVersion(implementor.getPluginVersion())
|
||||
.addAttribution("Author",
|
||||
AboutMenu.Credit.of("William278").withDescription("Click to visit website").withUrl("https://william278.net"))
|
||||
.addAttribution("Contributors",
|
||||
AboutMenu.Credit.of("HarvelsX").withDescription("Code"),
|
||||
AboutMenu.Credit.of("HookWoods").withDescription("Code"))
|
||||
.addAttribution("Translators",
|
||||
AboutMenu.Credit.of("Namiu").withDescription("Japanese (ja-jp)"),
|
||||
AboutMenu.Credit.of("anchelthe").withDescription("Spanish (es-es)"),
|
||||
AboutMenu.Credit.of("Melonzio").withDescription("Spanish (es-es)"),
|
||||
AboutMenu.Credit.of("Ceddix").withDescription("German (de-de)"),
|
||||
AboutMenu.Credit.of("Pukejoy_1").withDescription("Bulgarian (bg-bg)"),
|
||||
AboutMenu.Credit.of("mateusneresrb").withDescription("Brazilian Portuguese (pt-br)"),
|
||||
AboutMenu.Credit.of("小蔡").withDescription("Traditional Chinese (zh-tw)"),
|
||||
AboutMenu.Credit.of("Ghost-chu").withDescription("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("DJelly4K").withDescription("Simplified Chinese (zh-cn)"),
|
||||
AboutMenu.Credit.of("Thourgard").withDescription("Ukrainian (uk-ua)"),
|
||||
AboutMenu.Credit.of("xF3d3").withDescription("Italian (it-it)"))
|
||||
.addButtons(
|
||||
AboutMenu.Link.of("https://william278.net/docs/husksync").withText("Documentation").withIcon("⛏"),
|
||||
AboutMenu.Link.of("https://github.com/WiIIiam278/HuskSync/issues").withText("Issues").withIcon("❌").withColor("#ff9f0f"),
|
||||
AboutMenu.Link.of("https://discord.gg/tVYhJfyDWG").withText("Discord").withIcon("⭐").withColor("#6773f5"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
displayPluginInformation(player);
|
||||
sendAboutMenu(player);
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
@@ -34,18 +59,16 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter());
|
||||
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
|
||||
if (updateChecker.isUpdateAvailable(latestVersion)) {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + latestVersion + "](#00fb9a bold)" +
|
||||
"[•](white) [Currently running:](#00fb9a) [Version " + updateChecker.getCurrentVersion() + "](gray)" +
|
||||
"[•](white) [Download links:](#00fb9a) [[⏩ Spigot]](gray open_url=https://www.spigotmc.org/resources/husksync.97144/updates) [•](#262626) [[⏩ Polymart]](gray open_url=https://polymart.org/resource/husksync.1634/updates) [•](#262626) [[⏩ Songoda]](gray open_url=https://songoda.com/marketplace/product/husksync-a-modern-cross-server-player-data-synchronization-system.758)"));
|
||||
} else {
|
||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date, running version " + updateChecker.getCurrentVersion() + "](#00fb9a)"));
|
||||
}
|
||||
});
|
||||
plugin.getLatestVersionIfOutdated().thenAccept(newestVersion ->
|
||||
newestVersion.ifPresentOrElse(
|
||||
newVersion -> player.sendMessage(
|
||||
new MineDown("[HuskSync](#00fb9a bold) [| A new version of HuskSync is available!"
|
||||
+ " (v" + newVersion + " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)")),
|
||||
() -> player.sendMessage(
|
||||
new MineDown("[HuskSync](#00fb9a bold) [| HuskSync is up-to-date."
|
||||
+ " (Running: v" + plugin.getPluginVersion() + ")](#00fb9a)"))));
|
||||
}
|
||||
case "info", "about" -> displayPluginInformation(player);
|
||||
case "about", "info" -> sendAboutMenu(player);
|
||||
case "reload" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_RELOAD.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
@@ -69,11 +92,14 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
||||
return;
|
||||
}
|
||||
switch (args[0].toLowerCase()) {
|
||||
case "update", "version" ->
|
||||
new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()).logToConsole();
|
||||
case "info", "about" ->
|
||||
plugin.getLoggingAdapter().log(Level.INFO, new MineDown(plugin.getLocales().stripMineDown(
|
||||
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString()))));
|
||||
case "update", "version" -> plugin.getLatestVersionIfOutdated().thenAccept(newestVersion ->
|
||||
newestVersion.ifPresentOrElse(newVersion -> plugin.getLoggingAdapter().log(Level.WARNING,
|
||||
"An update is available for HuskSync, v" + newVersion
|
||||
+ " (Running v" + plugin.getPluginVersion() + ")"),
|
||||
() -> plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"HuskSync is up to date" +
|
||||
" (Running v" + plugin.getPluginVersion() + ")")));
|
||||
case "about", "info" -> aboutMenu.toString().lines().forEach(plugin.getLoggingAdapter()::info);
|
||||
case "reload" -> {
|
||||
plugin.reload();
|
||||
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files.");
|
||||
@@ -107,7 +133,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
||||
}, () -> {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"Please specify a valid migrator.\n" +
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
"If a migrator is not available, please verify that you meet the prerequisites to use it.");
|
||||
logMigratorsList();
|
||||
});
|
||||
}
|
||||
@@ -119,23 +145,26 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
||||
private void logMigratorsList() {
|
||||
plugin.getLoggingAdapter().log(Level.INFO,
|
||||
"List of available migrators:\nMigrator ID / Migrator Name:\n" +
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
|
||||
.collect(Collectors.joining("\n")));
|
||||
plugin.getAvailableMigrators().stream()
|
||||
.map(migrator -> migrator.getIdentifier() + " - " + migrator.getName())
|
||||
.collect(Collectors.joining("\n")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(@NotNull String[] args) {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
if (args.length <= 1) {
|
||||
return Arrays.stream(SUB_COMMANDS)
|
||||
.filter(argument -> argument.startsWith(args.length == 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
private void displayPluginInformation(@NotNull OnlineUser player) {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {
|
||||
private void sendAboutMenu(@NotNull OnlineUser player) {
|
||||
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_ABOUT.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString())));
|
||||
player.sendMessage(aboutMenu.toMineDown());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package net.william278.husksync.command;
|
||||
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.data.UserDataBuilder;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -58,23 +58,40 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
|
||||
@NotNull User dataOwner, boolean allowEdit) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
final UserData data = userDataSnapshot.userData();
|
||||
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
|
||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
plugin.getDataEditor().openItemEditorMenu(player, menu).thenAccept(inventoryDataOnClose -> {
|
||||
if (!menu.canEdit) {
|
||||
return;
|
||||
}
|
||||
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
|
||||
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
|
||||
data.getStatisticsData(), data.getLocationData(),
|
||||
data.getPersistentDataContainerData(),
|
||||
plugin.getMinecraftVersion().toString());
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
data.getInventory().ifPresent(itemData -> {
|
||||
// Show message
|
||||
plugin.getLocales().getLocale("inventory_viewer_opened", dataOwner.username,
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss")
|
||||
.format(userDataSnapshot.versionTimestamp()))
|
||||
.ifPresent(player::sendMessage);
|
||||
|
||||
// Show inventory menu
|
||||
player.showMenu(itemData, allowEdit, 5, plugin.getLocales()
|
||||
.getLocale("inventory_viewer_menu_title", dataOwner.username)
|
||||
.orElse(new MineDown("Inventory Viewer")))
|
||||
.thenAccept(dataOnClose -> {
|
||||
if (dataOnClose.isEmpty() || !allowEdit) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.getLoggingAdapter().debug("Inventory data changed, updating user, etc!");
|
||||
|
||||
// Create the updated data
|
||||
final UserDataBuilder builder = UserData.builder(plugin.getMinecraftVersion());
|
||||
data.getStatus().ifPresent(builder::setStatus);
|
||||
data.getAdvancements().ifPresent(builder::setAdvancements);
|
||||
data.getLocation().ifPresent(builder::setLocation);
|
||||
data.getPersistentDataContainer().ifPresent(builder::setPersistentDataContainer);
|
||||
data.getStatistics().ifPresent(builder::setStatistics);
|
||||
data.getPotionEffects().ifPresent(builder::setPotionEffects);
|
||||
data.getEnderChest().ifPresent(builder::setEnderChest);
|
||||
builder.setInventory(dataOnClose.get());
|
||||
|
||||
// Set the updated data
|
||||
final UserData updatedUserData = builder.build();
|
||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND).join();
|
||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public enum Permission {
|
||||
/**
|
||||
* Lets the user view plugin info {@code /husksync info}
|
||||
*/
|
||||
COMMAND_HUSKSYNC_INFO("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
COMMAND_HUSKSYNC_ABOUT("husksync.command.husksync.info", DefaultAccess.EVERYONE),
|
||||
/**
|
||||
* Lets the user reload the plugin {@code /husksync reload}
|
||||
*/
|
||||
@@ -41,6 +41,11 @@ public enum Permission {
|
||||
*/
|
||||
COMMAND_USER_DATA_MANAGE("husksync.command.userdata.manage", DefaultAccess.OPERATORS),
|
||||
|
||||
/**
|
||||
* Lets the user dump user data to a file or the web {@code /userdata dump (player) (version_uuid)}
|
||||
*/
|
||||
COMMAND_USER_DATA_DUMP("husksync.command.userdata.dump", DefaultAccess.NOBODY),
|
||||
|
||||
/*
|
||||
* /inventory command permissions
|
||||
*/
|
||||
|
||||
@@ -2,19 +2,24 @@ package net.william278.husksync.command;
|
||||
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.DataSaveCause;
|
||||
import net.william278.husksync.data.UserData;
|
||||
import net.william278.husksync.util.DataSnapshotList;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.util.DataDumper;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
|
||||
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin"};
|
||||
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin", "dump"};
|
||||
|
||||
public UserDataCommand(@NotNull HuskSync implementor) {
|
||||
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
|
||||
@@ -24,7 +29,7 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||
if (args.length < 1) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata <view/list/delete/restore/pin> <username> [version_uuid]")
|
||||
"/userdata <view/list/delete/restore/pin/dump> <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
@@ -41,31 +46,32 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data ->
|
||||
data.ifPresentOrElse(userData -> plugin.getDataEditor()
|
||||
.displayDataOverview(player, userData, user),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser
|
||||
.ifPresentOrElse(user -> plugin.getDatabase().getUserData(user, versionUuid)
|
||||
.thenAccept(data -> data.ifPresentOrElse(
|
||||
userData -> userData.displayDataOverview(player, user, plugin.getLocales()),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata view <username> [version_uuid]")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
} else {
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getCurrentUserData(user).thenAccept(
|
||||
latestData -> latestData.ifPresentOrElse(
|
||||
userData -> plugin.getDataEditor()
|
||||
.displayDataOverview(player, userData, user),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser
|
||||
.ifPresentOrElse(user -> plugin.getDatabase().getCurrentUserData(user)
|
||||
.thenAccept(latestData -> latestData.ifPresentOrElse(
|
||||
userData -> userData.displayDataOverview(player, user, plugin.getLocales()),
|
||||
() -> plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
}
|
||||
}
|
||||
case "list" -> {
|
||||
@@ -75,20 +81,38 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
}
|
||||
if (args.length < 2) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata list <username>")
|
||||
"/userdata list <username> [page]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
final String username = args[1];
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user).thenAccept(dataList -> {
|
||||
// Check if there is data to display
|
||||
if (dataList.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_no_data_to_display")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDataEditor().displayDataList(player, dataList, user);
|
||||
|
||||
// Determine page to display
|
||||
int page = 1;
|
||||
if (args.length >= 3) {
|
||||
try {
|
||||
page = Integer.parseInt(args[2]);
|
||||
} catch (NumberFormatException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata list <username> [page]")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the list to the player
|
||||
DataSnapshotList.create(dataList, user, plugin.getLocales())
|
||||
.displayPage(player, page);
|
||||
}),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
@@ -108,8 +132,9 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().deleteUserData(user, versionUuid).thenAccept(deleted -> {
|
||||
if (deleted) {
|
||||
plugin.getLocales().getLocale("data_deleted",
|
||||
@@ -146,16 +171,22 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(data -> {
|
||||
if (data.isEmpty()) {
|
||||
plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
plugin.getDatabase().setUserData(user, data.get().userData(),
|
||||
DataSaveCause.BACKUP_RESTORE);
|
||||
|
||||
// Restore users with a minimum of one health (prevent restoring players with <=0 health)
|
||||
final UserData userData = data.get().userData();
|
||||
userData.getStatus().ifPresent(status -> status.health = Math.max(1, status.health));
|
||||
|
||||
// Set the users data and send a message
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.BACKUP_RESTORE);
|
||||
plugin.getRedisManager().sendUserDataUpdate(user, data.get().userData()).join();
|
||||
plugin.getLocales().getLocale("data_restored",
|
||||
user.username,
|
||||
@@ -183,11 +214,13 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||
optionalUser -> optionalUser.ifPresentOrElse(
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||
if (userData.pinned()) {
|
||||
@@ -217,6 +250,46 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
case "dump" -> {
|
||||
if (!player.hasPermission(Permission.COMMAND_USER_DATA_DUMP.node)) {
|
||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
if (args.length < 3) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean toWeb = args.length > 3 && args[3].equalsIgnoreCase("web");
|
||||
final String username = args[1];
|
||||
try {
|
||||
final UUID versionUuid = UUID.fromString(args[2]);
|
||||
CompletableFuture.runAsync(() -> plugin.getDatabase()
|
||||
.getUserByName(username.toLowerCase())
|
||||
.thenAccept(optionalUser -> optionalUser.ifPresentOrElse(
|
||||
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||
try {
|
||||
final DataDumper dumper = DataDumper.create(userData, user, plugin);
|
||||
final String result = toWeb ? dumper.toWeb() : dumper.toFile();
|
||||
plugin.getLocales().getLocale("data_dumped", versionUuid.toString()
|
||||
.split("-")[0], user.username, result)
|
||||
.ifPresent(player::sendMessage);
|
||||
} catch (IOException e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to dump user data", e);
|
||||
}
|
||||
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||
.ifPresent(player::sendMessage))),
|
||||
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||
.ifPresent(player::sendMessage))));
|
||||
} catch (IllegalArgumentException e) {
|
||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||
"/userdata dump <username> <version_uuid>")
|
||||
.ifPresent(player::sendMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +298,7 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||
switch (args.length) {
|
||||
case 0, 1 -> {
|
||||
return Arrays.stream(COMMAND_ARGUMENTS)
|
||||
.filter(argument -> argument.startsWith(args.length >= 1 ? args[0] : ""))
|
||||
.filter(argument -> argument.startsWith(args.length == 1 ? args[0] : ""))
|
||||
.sorted().collect(Collectors.toList());
|
||||
}
|
||||
case 2 -> {
|
||||
|
||||
@@ -1,54 +1,49 @@
|
||||
package net.william278.husksync.config;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import dev.dejvokep.boostedyaml.YamlDocument;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
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;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
public static final String PLUGIN_INFORMATION = """
|
||||
[HuskSync](#00fb9a bold) [| Version %version%](#00fb9a)
|
||||
[A modern, cross-server player data synchronization system](gray)
|
||||
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net)
|
||||
[• Contributors:](white) [HarvelsX](gray show_text=&7Code), [HookWoods](gray show_text=&7Code)
|
||||
[• Translators:](white) [Namiu](gray show_text=&7\\(うにたろう\\) - Japanese, ja-jp), [anchelthe](gray show_text=&7Spanish, es-es), [Melonzio](gray show_text=&7Spanish, es-es), [Ceddix](gray show_text=&7German, de-de), [mateusneresrb](gray show_text=&7Brazilian Portuguese, pt-br], [小蔡](gray show_text=&7Traditional Chinese, zh-tw), [Ghost-chu](gray show_text=&7Simplified Chinese, zh-cn), [DJelly4K](gray show_text=&7Simplified Chinese, zh-cn), [Thourgard](gray show_text=&7Ukrainian, uk-ua), [xF3d3](gray show_text=&7Italian, it-it)
|
||||
[• Documentation:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://william278.net/docs/husksync/Home/)
|
||||
[• Bug reporting:](white) [[Link]](#00fb9a show_text=&7Click to open link open_url=https://github.com/WiIIiam278/HuskSync/issues)
|
||||
[• Discord support:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public Optional<String> getRawLocale(@NotNull String localeId) {
|
||||
if (rawLocales.containsKey(localeId)) {
|
||||
return Optional.of(rawLocales.get(localeId).replaceAll(Pattern.quote("\\n"), "\n"));
|
||||
}
|
||||
return Optional.empty();
|
||||
return Optional.ofNullable(rawLocales.get(localeId)).map(StringEscapeUtils::unescapeJava);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -70,13 +65,16 @@ public class Locales {
|
||||
|
||||
/**
|
||||
* 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 Ordered array of replacement strings to fill in placeholders with
|
||||
* @return An {@link Optional} containing the replacement-applied, formatted locale corresponding to the id, if it exists
|
||||
*/
|
||||
public Optional<MineDown> getLocale(@NotNull String localeId, @NotNull String... replacements) {
|
||||
return getRawLocale(localeId, replacements).map(MineDown::new);
|
||||
return getRawLocale(localeId, Arrays.stream(replacements).map(Locales::escapeMineDown)
|
||||
.toArray(String[]::new)).map(MineDown::new);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,54 +84,75 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the locales from a BoostedYaml {@link YamlDocument} locales file
|
||||
* 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 localesConfig The loaded {@link YamlDocument} locales.yml file
|
||||
* @return the loaded {@link Locales}
|
||||
* @param string The string to escape
|
||||
* @return The escaped string
|
||||
*/
|
||||
public static Locales load(@NotNull YamlDocument localesConfig) {
|
||||
return new Locales(localesConfig);
|
||||
@NotNull
|
||||
public static String escapeMineDown(@NotNull String string) {
|
||||
final StringBuilder value = new StringBuilder();
|
||||
for (int i = 0; i < string.length(); ++i) {
|
||||
char c = string.charAt(i);
|
||||
boolean isEscape = c == '\\';
|
||||
boolean isColorCode = i + 1 < string.length() && (c == 167 || c == '&');
|
||||
boolean isEvent = c == '[' || c == ']' || c == '(' || c == ')';
|
||||
if (isEscape || isColorCode || isEvent) {
|
||||
value.append('\\');
|
||||
}
|
||||
|
||||
value.append(c);
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips a string of basic MineDown formatting, used for displaying plugin info to console
|
||||
* Returns the base list options to use for a paginated chat list
|
||||
*
|
||||
* @param string The string to strip
|
||||
* @return The MineDown-stripped string
|
||||
* @param itemsPerPage The number of items to display per page
|
||||
* @return The list options
|
||||
*/
|
||||
public String stripMineDown(@NotNull String string) {
|
||||
final String[] in = string.split("\n");
|
||||
final StringBuilder out = new StringBuilder();
|
||||
String regex = "[^\\[\\]() ]*\\[([^()]+)]\\([^()]+open_url=(\\S+).*\\)";
|
||||
@NotNull
|
||||
public ListOptions.Builder getBaseChatList(int itemsPerPage) {
|
||||
return new ListOptions.Builder()
|
||||
.setFooterFormat(getRawLocale("list_footer",
|
||||
"%previous_page_button%", "%current_page%",
|
||||
"%total_pages%", "%next_page_button%", "%page_jumpers%").orElse(""))
|
||||
.setNextButtonFormat(getRawLocale("list_next_page_button",
|
||||
"%next_page_index%", "%command%").orElse(""))
|
||||
.setPreviousButtonFormat(getRawLocale("list_previous_page_button",
|
||||
"%previous_page_index%", "%command%").orElse(""))
|
||||
.setPageJumpersFormat(getRawLocale("list_page_jumpers",
|
||||
"%page_jump_buttons%").orElse(""))
|
||||
.setPageJumperPageFormat(getRawLocale("list_page_jumper_button",
|
||||
"%target_page_index%", "%command%").orElse(""))
|
||||
.setPageJumperCurrentPageFormat(getRawLocale("list_page_jumper_current_page",
|
||||
"%current_page%").orElse(""))
|
||||
.setPageJumperPageSeparator(getRawLocale("list_page_jumper_separator").orElse(""))
|
||||
.setPageJumperGroupSeparator(getRawLocale("list_page_jumper_group_separator").orElse(""))
|
||||
.setItemsPerPage(itemsPerPage)
|
||||
.setEscapeItemsMineDown(false)
|
||||
.setSpaceAfterHeader(false)
|
||||
.setSpaceBeforeFooter(false);
|
||||
}
|
||||
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher m = pattern.matcher(in[i]);
|
||||
|
||||
if (m.find()) {
|
||||
out.append(in[i].replace(m.group(0), ""));
|
||||
out.append(m.group(2));
|
||||
} else {
|
||||
out.append(in[i]);
|
||||
}
|
||||
|
||||
if (i + 1 != in.length) {
|
||||
out.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
return out.toString();
|
||||
@SuppressWarnings("unused")
|
||||
public Locales() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,272 +1,189 @@
|
||||
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.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
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 HashMap<ConfigOption, Object> configOptions;
|
||||
// Top-level settings
|
||||
public String language = "en-gb";
|
||||
|
||||
// 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);
|
||||
}));
|
||||
@YamlKey("check_for_updates")
|
||||
public boolean checkForUpdates = true;
|
||||
|
||||
@YamlKey("cluster_id")
|
||||
public String clusterId = "";
|
||||
|
||||
@YamlKey("debug_logging")
|
||||
public boolean debugLogging = false;
|
||||
|
||||
|
||||
// Database settings
|
||||
@YamlComment("Database connection settings")
|
||||
@YamlKey("database.credentials.host")
|
||||
public String mySqlHost = "localhost";
|
||||
|
||||
@YamlKey("database.credentials.port")
|
||||
public int mySqlPort = 3306;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a boolean
|
||||
* @throws ClassCastException if the option is not a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (Boolean) configOptions.get(option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the specified {@link ConfigOption}
|
||||
*
|
||||
* @param option the {@link ConfigOption} to check
|
||||
* @return the value of the {@link ConfigOption} as a string
|
||||
* @throws ClassCastException if the option is not a string
|
||||
*/
|
||||
public String getStringValue(@NotNull ConfigOption option) throws ClassCastException {
|
||||
return (String) configOptions.get(option);
|
||||
}
|
||||
// Redis settings
|
||||
@YamlComment("Redis connection settings")
|
||||
@YamlKey("redis.credentials.host")
|
||||
public String redisHost = "localhost";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@YamlKey("redis.credentials.port")
|
||||
public int redisPort = 6379;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
@YamlKey("redis.credentials.password")
|
||||
public String redisPassword = "";
|
||||
|
||||
/**
|
||||
* 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("redis.use_ssl")
|
||||
public boolean redisUseSsl = 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 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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Load the settings from a BoostedYaml {@link YamlDocument} config file
|
||||
*
|
||||
* @param config The loaded {@link YamlDocument} config.yml file
|
||||
* @return the loaded {@link Settings}
|
||||
* Represents the names of tables in the database
|
||||
*/
|
||||
public static Settings load(@NotNull YamlDocument config) {
|
||||
return new Settings(config);
|
||||
public enum TableName {
|
||||
USERS("husksync_users"),
|
||||
USER_DATA("husksync_user_data");
|
||||
|
||||
private final String defaultName;
|
||||
|
||||
TableName(@NotNull String defaultName) {
|
||||
this.defaultName = defaultName;
|
||||
}
|
||||
|
||||
private Map.Entry<String, String> toEntry() {
|
||||
return Map.entry(name().toLowerCase(), defaultName);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, String> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(TableName::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an option stored by a path in config.yml
|
||||
* Represents enabled synchronisation features
|
||||
*/
|
||||
public enum ConfigOption {
|
||||
LANGUAGE("language", OptionType.STRING, "en-gb"),
|
||||
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
|
||||
public enum SynchronizationFeature {
|
||||
|
||||
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
|
||||
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false),
|
||||
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);
|
||||
|
||||
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"),
|
||||
private final boolean enabledByDefault;
|
||||
|
||||
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_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;
|
||||
SynchronizationFeature(boolean enabledByDefault) {
|
||||
this.enabledByDefault = enabledByDefault;
|
||||
}
|
||||
|
||||
ConfigOption(@NotNull String configPath, @NotNull OptionType optionType) {
|
||||
this.configPath = configPath;
|
||||
this.optionType = optionType;
|
||||
this.defaultValue = null;
|
||||
private Map.Entry<String, Boolean> toEntry() {
|
||||
return Map.entry(name().toLowerCase(), enabledByDefault);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string
|
||||
*/
|
||||
public String getStringValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getString(configPath, (String) defaultValue)
|
||||
: config.getString(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a boolean
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a boolean
|
||||
*/
|
||||
public boolean getBooleanValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getBoolean(configPath, (Boolean) defaultValue)
|
||||
: config.getBoolean(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a double
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a double
|
||||
*/
|
||||
public double getDoubleValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getDouble(configPath, (Double) defaultValue)
|
||||
: config.getDouble(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a float
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a float
|
||||
*/
|
||||
public float getFloatValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getFloat(configPath, (Float) defaultValue)
|
||||
: config.getFloat(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as an int
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as an int
|
||||
*/
|
||||
public int getIntValue(@NotNull YamlDocument config) {
|
||||
return defaultValue != null
|
||||
? config.getInt(configPath, (Integer) defaultValue)
|
||||
: config.getInt(configPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the path specified (or return default if set), as a string {@link List}
|
||||
*
|
||||
* @param config The {@link YamlDocument} config file
|
||||
* @return the value defined in the config, as a string {@link List}
|
||||
*/
|
||||
public List<String> getStringListValue(@NotNull YamlDocument config) {
|
||||
return config.getStringList(configPath, new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the type of the object
|
||||
*/
|
||||
public enum OptionType {
|
||||
BOOLEAN,
|
||||
STRING,
|
||||
DOUBLE,
|
||||
FLOAT,
|
||||
INTEGER,
|
||||
STRING_LIST
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Boolean> getDefaults() {
|
||||
return Map.ofEntries(Arrays.stream(values())
|
||||
.map(SynchronizationFeature::toEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents the type of a {@link PersistentDataTag}
|
||||
*/
|
||||
public enum BukkitPersistentDataTagType {
|
||||
|
||||
BYTE,
|
||||
SHORT,
|
||||
INTEGER,
|
||||
LONG,
|
||||
FLOAT,
|
||||
DOUBLE,
|
||||
STRING,
|
||||
BYTE_ARRAY,
|
||||
INTEGER_ARRAY,
|
||||
LONG_ARRAY,
|
||||
TAG_CONTAINER_ARRAY,
|
||||
TAG_CONTAINER;
|
||||
|
||||
|
||||
public static Optional<BukkitPersistentDataTagType> getDataType(@NotNull String typeName) {
|
||||
for (BukkitPersistentDataTagType type : values()) {
|
||||
if (type.name().equalsIgnoreCase(typeName)) {
|
||||
return Optional.of(type);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -26,6 +26,12 @@ public enum DataSaveCause {
|
||||
* @since 2.0
|
||||
*/
|
||||
WORLD_SAVE,
|
||||
/**
|
||||
* Indicates data saved when the user died
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
DEATH,
|
||||
/**
|
||||
* Indicates data saved when the server shut down
|
||||
*
|
||||
|
||||
@@ -14,6 +14,16 @@ public class ItemData {
|
||||
@SerializedName("serialized_items")
|
||||
public String serializedItems;
|
||||
|
||||
/**
|
||||
* Get an empty item data object, representing an empty inventory or Ender Chest
|
||||
*
|
||||
* @return an empty item data object
|
||||
*/
|
||||
@NotNull
|
||||
public static ItemData empty() {
|
||||
return new ItemData("");
|
||||
}
|
||||
|
||||
public ItemData(@NotNull final String serializedItems) {
|
||||
this.serializedItems = serializedItems;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import com.google.gson.annotations.SerializedName;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Store's a user's persistent data container, holding a map of plugin-set persistent values
|
||||
@@ -14,9 +16,9 @@ public class PersistentDataContainerData {
|
||||
* Map of namespaced key strings to a byte array representing the persistent data
|
||||
*/
|
||||
@SerializedName("persistent_data_map")
|
||||
public Map<String, Byte[]> persistentDataMap;
|
||||
protected Map<String, PersistentDataTag<?>> persistentDataMap;
|
||||
|
||||
public PersistentDataContainerData(@NotNull final Map<String, Byte[]> persistentDataMap) {
|
||||
public PersistentDataContainerData(@NotNull final Map<String, PersistentDataTag<?>> persistentDataMap) {
|
||||
this.persistentDataMap = persistentDataMap;
|
||||
}
|
||||
|
||||
@@ -24,4 +26,23 @@ public class PersistentDataContainerData {
|
||||
protected PersistentDataContainerData() {
|
||||
}
|
||||
|
||||
|
||||
public <T> Optional<T> getTagValue(@NotNull final String tagName, @NotNull Class<T> tagClass) {
|
||||
if (persistentDataMap.containsKey(tagName)) {
|
||||
return Optional.of(tagClass.cast(persistentDataMap.get(tagName).value));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Optional<BukkitPersistentDataTagType> getTagType(@NotNull final String tagType) {
|
||||
if (persistentDataMap.containsKey(tagType)) {
|
||||
return BukkitPersistentDataTagType.getDataType(persistentDataMap.get(tagType).type);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Set<String> getTags() {
|
||||
return persistentDataMap.keySet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Represents a persistent data tag set by a plugin.
|
||||
*/
|
||||
public class PersistentDataTag<T> {
|
||||
|
||||
/**
|
||||
* The enumerated primitive data type name value of the tag
|
||||
*/
|
||||
protected String type;
|
||||
|
||||
/**
|
||||
* The value of the tag
|
||||
*/
|
||||
public T value;
|
||||
|
||||
public PersistentDataTag(@NotNull BukkitPersistentDataTagType type, @NotNull T value) {
|
||||
this.type = type.name();
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
private PersistentDataTag() {
|
||||
}
|
||||
|
||||
public Optional<BukkitPersistentDataTagType> getType() {
|
||||
return BukkitPersistentDataTagType.getDataType(type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.william278.desertwell.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/***
|
||||
/**
|
||||
* Stores data about a user
|
||||
*/
|
||||
public class UserData {
|
||||
@@ -15,72 +18,97 @@ public class UserData {
|
||||
* </p>
|
||||
* This value is to be incremented whenever the format changes.
|
||||
*/
|
||||
public static final int CURRENT_FORMAT_VERSION = 1;
|
||||
public static final int CURRENT_FORMAT_VERSION = 3;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
*/
|
||||
@SerializedName("status")
|
||||
@Nullable
|
||||
protected StatusData statusData;
|
||||
|
||||
/**
|
||||
* Stores the user's inventory contents
|
||||
*/
|
||||
@SerializedName("inventory")
|
||||
@Nullable
|
||||
protected ItemData inventoryData;
|
||||
|
||||
/**
|
||||
* Stores the user's ender chest contents
|
||||
*/
|
||||
@SerializedName("ender_chest")
|
||||
@Nullable
|
||||
protected ItemData enderChestData;
|
||||
|
||||
/**
|
||||
* Store's the user's potion effects
|
||||
*/
|
||||
@SerializedName("potion_effects")
|
||||
@Nullable
|
||||
protected PotionEffectData potionEffectData;
|
||||
|
||||
/**
|
||||
* Stores the set of this user's advancements
|
||||
*/
|
||||
@SerializedName("advancements")
|
||||
@Nullable
|
||||
protected List<AdvancementData> advancementData;
|
||||
|
||||
/**
|
||||
* Stores the user's set of statistics
|
||||
*/
|
||||
@SerializedName("statistics")
|
||||
@Nullable
|
||||
protected StatisticsData statisticData;
|
||||
|
||||
/**
|
||||
* Store's the user's world location and coordinates
|
||||
*/
|
||||
@SerializedName("location")
|
||||
@Nullable
|
||||
protected LocationData locationData;
|
||||
|
||||
/**
|
||||
* Stores the user's serialized persistent data container, which contains metadata keys applied by other plugins
|
||||
*/
|
||||
@SerializedName("persistent_data_container")
|
||||
@Nullable
|
||||
protected PersistentDataContainerData persistentDataContainerData;
|
||||
|
||||
/**
|
||||
* Stores the version of Minecraft this data was generated in
|
||||
*/
|
||||
@SerializedName("minecraft_version")
|
||||
@NotNull
|
||||
protected String minecraftVersion;
|
||||
|
||||
/**
|
||||
* Stores the version of the data format being used
|
||||
*/
|
||||
@SerializedName("format_version")
|
||||
protected int formatVersion;
|
||||
protected int formatVersion = CURRENT_FORMAT_VERSION;
|
||||
|
||||
public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData,
|
||||
@NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData,
|
||||
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
|
||||
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData,
|
||||
/**
|
||||
* Create a new {@link UserData} object with the provided data
|
||||
*
|
||||
* @param statusData the user's status data ({@link StatusData})
|
||||
* @param inventoryData the user's inventory data ({@link ItemData})
|
||||
* @param enderChestData the user's ender chest data ({@link ItemData})
|
||||
* @param potionEffectData the user's potion effect data ({@link PotionEffectData})
|
||||
* @param advancementData the user's advancement data ({@link AdvancementData})
|
||||
* @param statisticData the user's statistic data ({@link StatisticsData})
|
||||
* @param locationData the user's location data ({@link LocationData})
|
||||
* @param persistentDataContainerData the user's persistent data container data ({@link PersistentDataContainerData})
|
||||
* @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"})
|
||||
* @deprecated see {@link #builder(String)} or {@link #builder(Version)} to create a {@link UserDataBuilder}, which
|
||||
* you can use to {@link UserDataBuilder#build()} a {@link UserData} instance with
|
||||
*/
|
||||
@Deprecated(since = "2.1")
|
||||
public UserData(@Nullable StatusData statusData, @Nullable ItemData inventoryData,
|
||||
@Nullable ItemData enderChestData, @Nullable PotionEffectData potionEffectData,
|
||||
@Nullable List<AdvancementData> advancementData, @Nullable StatisticsData statisticData,
|
||||
@Nullable LocationData locationData, @Nullable PersistentDataContainerData persistentDataContainerData,
|
||||
@NotNull String minecraftVersion) {
|
||||
this.statusData = statusData;
|
||||
this.inventoryData = inventoryData;
|
||||
@@ -91,7 +119,6 @@ public class UserData {
|
||||
this.locationData = locationData;
|
||||
this.persistentDataContainerData = persistentDataContainerData;
|
||||
this.minecraftVersion = minecraftVersion;
|
||||
this.formatVersion = CURRENT_FORMAT_VERSION;
|
||||
}
|
||||
|
||||
// Empty constructor to facilitate json serialization
|
||||
@@ -99,45 +126,232 @@ public class UserData {
|
||||
protected UserData() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatusData} from this user data
|
||||
*
|
||||
* @return the {@link StatusData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getStatus()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public StatusData getStatusData() {
|
||||
return statusData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatusData} from this user data
|
||||
*
|
||||
* @return an optional containing the {@link StatusData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<StatusData> getStatus() {
|
||||
return Optional.ofNullable(statusData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's inventory from this user data
|
||||
*
|
||||
* @return the inventory {@link ItemData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getInventory()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public ItemData getInventoryData() {
|
||||
return inventoryData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's inventory from this user data
|
||||
*
|
||||
* @return an optional containing the inventory {@link ItemData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<ItemData> getInventory() {
|
||||
return Optional.ofNullable(inventoryData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's ender chest from this user data
|
||||
*
|
||||
* @return the ender chest {@link ItemData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getEnderChest()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public ItemData getEnderChestData() {
|
||||
return enderChestData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ItemData} representing the player's ender chest from this user data
|
||||
*
|
||||
* @return an optional containing the ender chest {@link ItemData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<ItemData> getEnderChest() {
|
||||
return Optional.ofNullable(enderChestData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PotionEffectData} representing player status effects from this user data
|
||||
*
|
||||
* @return the {@link PotionEffectData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getPotionEffects()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public PotionEffectData getPotionEffectsData() {
|
||||
return potionEffectData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PotionEffectData} representing the player's potion effects from this user data
|
||||
*
|
||||
* @return an optional containing {@link PotionEffectData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<PotionEffectData> getPotionEffects() {
|
||||
return Optional.ofNullable(potionEffectData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of {@link AdvancementData} from this user data
|
||||
*
|
||||
* @return the {@link AdvancementData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getAdvancements()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public List<AdvancementData> getAdvancementData() {
|
||||
return advancementData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of {@link AdvancementData} representing the player's advancements from this user data
|
||||
*
|
||||
* @return an optional containing a {@link List} of {@link AdvancementData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<List<AdvancementData>> getAdvancements() {
|
||||
return Optional.ofNullable(advancementData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link StatisticsData} representing player statistics from this user data
|
||||
*
|
||||
* @return the {@link StatisticsData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getStatistics()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public StatisticsData getStatisticsData() {
|
||||
return statisticData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link StatisticsData} representing player statistics from this user data
|
||||
*
|
||||
* @return an optional containing player {@link StatisticsData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<StatisticsData> getStatistics() {
|
||||
return Optional.ofNullable(statisticData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link LocationData} representing the player location from this user data
|
||||
*
|
||||
* @return the inventory {@link LocationData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getLocation()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public LocationData getLocationData() {
|
||||
return locationData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link LocationData} representing the player location from this user data
|
||||
*
|
||||
* @return an optional containing player {@link LocationData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<LocationData> getLocation() {
|
||||
return Optional.ofNullable(locationData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link PersistentDataContainerData} from this user data
|
||||
*
|
||||
* @return the {@link PersistentDataContainerData} of this user data
|
||||
* @since 2.0
|
||||
* @deprecated Use {@link #getPersistentDataContainer()}, which returns an optional instead
|
||||
*/
|
||||
@Nullable
|
||||
@Deprecated(since = "2.1")
|
||||
public PersistentDataContainerData getPersistentDataContainerData() {
|
||||
return persistentDataContainerData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets {@link PersistentDataContainerData} from this user data
|
||||
*
|
||||
* @return an optional containing the player's {@link PersistentDataContainerData} if it is present in this user data
|
||||
* @since 2.1
|
||||
*/
|
||||
public Optional<PersistentDataContainerData> getPersistentDataContainer() {
|
||||
return Optional.ofNullable(persistentDataContainerData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the version of Minecraft this data was generated in
|
||||
*
|
||||
* @return the version of Minecraft this data was generated in
|
||||
*/
|
||||
@NotNull
|
||||
public String getMinecraftVersion() {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the version of the data format being used
|
||||
*
|
||||
* @return the version of the data format being used
|
||||
*/
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link UserDataBuilder} for creating {@link UserData}
|
||||
*
|
||||
* @param minecraftVersion the version of Minecraft this data was generated in (e.g. {@code "1.19.2"})
|
||||
* @return a UserData {@link UserDataBuilder} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public static UserDataBuilder builder(@NotNull String minecraftVersion) {
|
||||
return new UserDataBuilder(minecraftVersion);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new {@link UserDataBuilder} for creating {@link UserData}
|
||||
*
|
||||
* @param minecraftVersion a {@link Version} object, representing the Minecraft version this data was generated in
|
||||
* @return a UserData {@link UserDataBuilder} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public static UserDataBuilder builder(@NotNull Version minecraftVersion) {
|
||||
return builder(minecraftVersion.toStringWithoutMetadata());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A builder utility for creating {@link UserData} instances
|
||||
*
|
||||
* @since 2.1
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
public class UserDataBuilder {
|
||||
|
||||
@NotNull
|
||||
private final UserData userData;
|
||||
|
||||
protected UserDataBuilder(@NotNull String minecraftVersion) {
|
||||
this.userData = new UserData();
|
||||
this.userData.minecraftVersion = minecraftVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link StatusData} to this {@link UserData}
|
||||
*
|
||||
* @param status the {@link StatusData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setStatus(@NotNull StatusData status) {
|
||||
this.userData.statusData = status;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the inventory {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param inventoryData the inventory {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setInventory(@Nullable ItemData inventoryData) {
|
||||
this.userData.inventoryData = inventoryData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ender chest {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param enderChestData the ender chest {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setEnderChest(@Nullable ItemData enderChestData) {
|
||||
this.userData.enderChestData = enderChestData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link List} of {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param potionEffectData the {@link List} of {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setPotionEffects(@Nullable PotionEffectData potionEffectData) {
|
||||
this.userData.potionEffectData = potionEffectData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link List} of {@link ItemData} to this {@link UserData}
|
||||
*
|
||||
* @param advancementData the {@link List} of {@link ItemData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setAdvancements(@Nullable List<AdvancementData> advancementData) {
|
||||
this.userData.advancementData = advancementData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link StatisticsData} to this {@link UserData}
|
||||
*
|
||||
* @param statisticData the {@link StatisticsData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setStatistics(@Nullable StatisticsData statisticData) {
|
||||
this.userData.statisticData = statisticData;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@link LocationData} to this {@link UserData}
|
||||
*
|
||||
* @param locationData the {@link LocationData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setLocation(@Nullable LocationData locationData) {
|
||||
this.userData.locationData = locationData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link PersistentDataContainerData} to this {@link UserData}
|
||||
*
|
||||
* @param persistentDataContainerData the {@link PersistentDataContainerData} to set
|
||||
* @return this {@link UserDataBuilder}
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserDataBuilder setPersistentDataContainer(@Nullable PersistentDataContainerData persistentDataContainerData) {
|
||||
this.userData.persistentDataContainerData = persistentDataContainerData;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build and get the {@link UserData} instance
|
||||
*
|
||||
* @return the {@link UserData} instance
|
||||
* @since 2.1
|
||||
*/
|
||||
@NotNull
|
||||
public UserData build() {
|
||||
return this.userData;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
@@ -32,6 +39,82 @@ public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionT
|
||||
DataSaveCause.API, false, userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a menu in chat to an {@link OnlineUser} about this {@link UserDataSnapshot} for a {@link User dataOwner}
|
||||
*
|
||||
* @param user The {@link OnlineUser} to display the menu to
|
||||
* @param dataOwner The {@link User} whose data this snapshot captures a state of
|
||||
* @param locales The {@link Locales} to use for displaying the menu
|
||||
*/
|
||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull User dataOwner, @NotNull Locales locales) {
|
||||
// Title message, timestamp, owner and cause.
|
||||
locales.getLocale("data_manager_title", versionUUID().toString().split("-")[0],
|
||||
versionUUID().toString(), dataOwner.username, dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(versionTimestamp()))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (pinned()) {
|
||||
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause", cause().name().toLowerCase().replaceAll("_", " "))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
userData().getStatus()
|
||||
.flatMap(statusData -> locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) statusData.health),
|
||||
Integer.toString((int) statusData.maxHealth),
|
||||
Integer.toString(statusData.hunger),
|
||||
Integer.toString(statusData.expLevel),
|
||||
statusData.gameMode.toLowerCase()))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
// Advancement and statistic data, if both are present in the snapshot
|
||||
userData().getAdvancements()
|
||||
.flatMap(advancementData -> userData().getStatistics()
|
||||
.flatMap(statisticsData -> locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(advancementData.size()),
|
||||
generateAdvancementPreview(advancementData, locales),
|
||||
String.format("%.2f", (((statisticsData.untypedStatistics.getOrDefault(
|
||||
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))))
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
|
||||
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
|
||||
locales.getLocale("data_manager_item_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
locales.getLocale("data_manager_management_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_DUMP.node)) {
|
||||
locales.getLocale("data_manager_system_buttons", dataOwner.username, versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData, @NotNull Locales locales) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementsToPreview.size(); i++) {
|
||||
joiner.add(advancementsToPreview.get(i).key);
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE;
|
||||
if (remainingAdvancements > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…"));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare UserData by creation timestamp
|
||||
*
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
@@ -108,7 +109,7 @@ public abstract class Database {
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException {
|
||||
return formatStatementTables(new String(resourceReader.getResource(schemaFileName)
|
||||
return formatStatementTables(new String(Objects.requireNonNull(resourceReader.getResource(schemaFileName))
|
||||
.readAllBytes(), StandardCharsets.UTF_8)).split(";");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +121,7 @@ public class MySqlDatabase extends Database {
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> ensureUser(@NotNull User user) {
|
||||
return CompletableFuture.runAsync(() -> getUser(user.uuid).thenAccept(optionalUser ->
|
||||
return getUser(user.uuid).thenAccept(optionalUser ->
|
||||
optionalUser.ifPresentOrElse(existingUser -> {
|
||||
if (!existingUser.username.equals(user.username)) {
|
||||
// Update a user's name if it has changed in the database
|
||||
@@ -155,7 +155,7 @@ public class MySqlDatabase extends Database {
|
||||
} catch (SQLException e) {
|
||||
getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e);
|
||||
}
|
||||
})));
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.AdvancementData;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Provides methods for displaying and editing user data
|
||||
*/
|
||||
public class DataEditor {
|
||||
|
||||
/**
|
||||
* Map of currently open inventory and ender chest data editors
|
||||
*/
|
||||
@NotNull
|
||||
protected final HashMap<UUID, ItemEditorMenu> openInventoryMenus;
|
||||
|
||||
private final Locales locales;
|
||||
|
||||
public DataEditor(@NotNull Locales locales) {
|
||||
this.openInventoryMenus = new HashMap<>();
|
||||
this.locales = locales;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an inventory or ender chest editor menu
|
||||
*
|
||||
* @param user The online user to open the editor for
|
||||
* @param itemEditorMenu The {@link ItemEditorMenu} to open
|
||||
* @see ItemEditorMenu#createInventoryMenu(ItemData, User, OnlineUser, Locales, boolean)
|
||||
* @see ItemEditorMenu#createEnderChestMenu(ItemData, User, OnlineUser, Locales, boolean)
|
||||
*/
|
||||
public CompletableFuture<ItemData> openItemEditorMenu(@NotNull OnlineUser user,
|
||||
@NotNull ItemEditorMenu itemEditorMenu) {
|
||||
this.openInventoryMenus.put(user.uuid, itemEditorMenu);
|
||||
return itemEditorMenu.showInventory(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close an inventory or ender chest editor menu
|
||||
*
|
||||
* @param user The online user to close the editor for
|
||||
* @param itemData the {@link ItemData} contained within the menu at the time of closing
|
||||
*/
|
||||
public void closeInventoryMenu(@NotNull OnlineUser user, @NotNull ItemData itemData) {
|
||||
if (this.openInventoryMenus.containsKey(user.uuid)) {
|
||||
this.openInventoryMenus.get(user.uuid).closeInventory(itemData);
|
||||
}
|
||||
this.openInventoryMenus.remove(user.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether edits to the inventory or ender chest menu are allowed
|
||||
*
|
||||
* @param user The online user with an inventory open to check
|
||||
* @return {@code true} if edits to the inventory or ender chest menu are allowed; {@code false} otherwise, including if they don't have an inventory open
|
||||
*/
|
||||
public boolean cancelMenuEdit(@NotNull OnlineUser user) {
|
||||
if (this.openInventoryMenus.containsKey(user.uuid)) {
|
||||
return !this.openInventoryMenus.get(user.uuid).canEdit;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a chat menu detailing information about {@link UserDataSnapshot}
|
||||
*
|
||||
* @param user The online user to display the message to
|
||||
* @param userData The {@link UserDataSnapshot} to display information about
|
||||
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||
*/
|
||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData,
|
||||
@NotNull User dataOwner) {
|
||||
locales.getLocale("data_manager_title",
|
||||
userData.versionUUID().toString().split("-")[0],
|
||||
userData.versionUUID().toString(),
|
||||
dataOwner.username,
|
||||
dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_timestamp",
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (userData.pinned()) {
|
||||
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||
}
|
||||
locales.getLocale("data_manager_cause",
|
||||
userData.cause().name().toLowerCase().replaceAll("_", " "))
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) userData.userData().getStatusData().health),
|
||||
Integer.toString((int) userData.userData().getStatusData().maxHealth),
|
||||
Integer.toString(userData.userData().getStatusData().hunger),
|
||||
Integer.toString(userData.userData().getStatusData().expLevel),
|
||||
userData.userData().getStatusData().gameMode.toLowerCase())
|
||||
.ifPresent(user::sendMessage);
|
||||
locales.getLocale("data_manager_advancements_statistics",
|
||||
Integer.toString(userData.userData().getAdvancementData().size()),
|
||||
generateAdvancementPreview(userData.userData().getAdvancementData()),
|
||||
String.format("%.2f", (((userData.userData().getStatisticsData().untypedStatistics.getOrDefault(
|
||||
"PLAY_ONE_MINUTE", 0)) / 20d) / 60d) / 60d))
|
||||
.ifPresent(user::sendMessage);
|
||||
if (user.hasPermission(Permission.COMMAND_INVENTORY.node)
|
||||
&& user.hasPermission(Permission.COMMAND_ENDER_CHEST.node)) {
|
||||
locales.getLocale("data_manager_item_buttons",
|
||||
dataOwner.username, userData.versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
if (user.hasPermission(Permission.COMMAND_USER_DATA_MANAGE.node)) {
|
||||
locales.getLocale("data_manager_management_buttons",
|
||||
dataOwner.username, userData.versionUUID().toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData) {
|
||||
final StringJoiner joiner = new StringJoiner("\n");
|
||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||
final int PREVIEW_SIZE = 8;
|
||||
for (int i = 0; i < advancementsToPreview.size(); i++) {
|
||||
joiner.add(advancementsToPreview.get(i).key);
|
||||
if (i >= PREVIEW_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final int remainingAdvancements = advancementsToPreview.size() - PREVIEW_SIZE;
|
||||
if (remainingAdvancements > 0) {
|
||||
joiner.add(locales.getRawLocale("data_manager_advancements_preview_remaining",
|
||||
Integer.toString(remainingAdvancements)).orElse("+" + remainingAdvancements + "…"));
|
||||
}
|
||||
return joiner.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a chat list detailing a player's saved list of {@link UserDataSnapshot}
|
||||
*
|
||||
* @param user The online user to display the message to
|
||||
* @param userDataList The list of {@link UserDataSnapshot} to display
|
||||
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||
*/
|
||||
public void displayDataList(@NotNull OnlineUser user, @NotNull List<UserDataSnapshot> userDataList,
|
||||
@NotNull User dataOwner) {
|
||||
locales.getLocale("data_list_title",
|
||||
dataOwner.username, dataOwner.uuid.toString())
|
||||
.ifPresent(user::sendMessage);
|
||||
|
||||
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
|
||||
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
|
||||
final UserDataSnapshot userData = userDataList.get(i);
|
||||
locales.getLocale("data_list_item",
|
||||
numberedIcons[i],
|
||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||
.format(userData.versionTimestamp()),
|
||||
userData.versionUUID().toString().split("-")[0],
|
||||
userData.versionUUID().toString(),
|
||||
userData.cause().name().toLowerCase().replaceAll("_", " "),
|
||||
dataOwner.username,
|
||||
userData.pinned() ? "※" : " ")
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the user has an inventory editor menu open
|
||||
*
|
||||
* @param user {@link OnlineUser} to check
|
||||
* @return {@code true} if the user has an inventory editor open; {@code false} otherwise
|
||||
*/
|
||||
public Optional<ItemEditorMenu> getEditingInventoryData(@NotNull OnlineUser user) {
|
||||
return this.openInventoryMenus.containsKey(user.uuid) ? Optional.of(this.openInventoryMenus.get(user.uuid))
|
||||
: Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.command.Permission;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.ItemData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ItemEditorMenu {
|
||||
|
||||
public final ItemData itemData;
|
||||
public final ItemEditorMenuType itemEditorMenuType;
|
||||
public final MineDown menuTitle;
|
||||
public final boolean canEdit;
|
||||
|
||||
private CompletableFuture<ItemData> inventoryDataCompletableFuture;
|
||||
|
||||
private ItemEditorMenu(@NotNull ItemData itemData, ItemEditorMenuType itemEditorMenuType,
|
||||
@NotNull MineDown menuTitle, boolean canEdit) {
|
||||
this.itemData = itemData;
|
||||
this.menuTitle = menuTitle;
|
||||
this.itemEditorMenuType = itemEditorMenuType;
|
||||
this.canEdit = canEdit;
|
||||
}
|
||||
|
||||
public CompletableFuture<ItemData> showInventory(@NotNull OnlineUser user) {
|
||||
inventoryDataCompletableFuture = new CompletableFuture<>();
|
||||
user.showMenu(this);
|
||||
return inventoryDataCompletableFuture;
|
||||
}
|
||||
|
||||
public void closeInventory(@NotNull ItemData itemData) {
|
||||
inventoryDataCompletableFuture.complete(itemData);
|
||||
}
|
||||
|
||||
public static ItemEditorMenu createInventoryMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
|
||||
@NotNull OnlineUser viewer, @NotNull Locales locales,
|
||||
boolean canEdit) {
|
||||
return new ItemEditorMenu(itemData, ItemEditorMenuType.INVENTORY_VIEWER,
|
||||
locales.getLocale(ItemEditorMenuType.INVENTORY_VIEWER.localeKey, dataOwner.username).orElse(new MineDown("")),
|
||||
viewer.hasPermission(Permission.COMMAND_INVENTORY_EDIT.node) && canEdit);
|
||||
}
|
||||
|
||||
public static ItemEditorMenu createEnderChestMenu(@NotNull ItemData itemData, @NotNull User dataOwner,
|
||||
@NotNull OnlineUser viewer, @NotNull Locales locales,
|
||||
boolean canEdit) {
|
||||
return new ItemEditorMenu(itemData, ItemEditorMenuType.ENDER_CHEST_VIEWER,
|
||||
locales.getLocale(ItemEditorMenuType.ENDER_CHEST_VIEWER.localeKey, dataOwner.username).orElse(new MineDown("")),
|
||||
viewer.hasPermission(Permission.COMMAND_ENDER_CHEST_EDIT.node) && canEdit);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package net.william278.husksync.editor;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public enum ItemEditorMenuType {
|
||||
INVENTORY_VIEWER(45, "inventory_viewer_menu_title"),
|
||||
ENDER_CHEST_VIEWER(27, "ender_chest_viewer_menu_title");
|
||||
|
||||
public final int slotCount;
|
||||
final String localeKey;
|
||||
|
||||
ItemEditorMenuType(int slotCount, @NotNull String localeKey) {
|
||||
this.slotCount = slotCount;
|
||||
this.localeKey = localeKey;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import com.djrapitops.plan.extension.icon.Family;
|
||||
import com.djrapitops.plan.extension.icon.Icon;
|
||||
import com.djrapitops.plan.extension.table.Table;
|
||||
import com.djrapitops.plan.extension.table.TableColumnFormat;
|
||||
import net.william278.husksync.data.StatusData;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.database.Database;
|
||||
import net.william278.husksync.player.User;
|
||||
@@ -114,9 +113,9 @@ public class PlanDataExtension implements DataExtension {
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getCurrentDataId(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> versionedUserData.versionUUID().toString()
|
||||
.split(Pattern.quote("-"))[0])
|
||||
return getCurrentUserData(uuid).join()
|
||||
.map(versionedUserData -> versionedUserData.versionUUID().toString()
|
||||
.split(Pattern.quote("-"))[0])
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@@ -130,11 +129,9 @@ public class PlanDataExtension implements DataExtension {
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> {
|
||||
final StatusData statusData = versionedUserData.userData().getStatusData();
|
||||
return (int) statusData.health + "/" + (int) statusData.maxHealth;
|
||||
})
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (int) statusData.health + "/" + (int) statusData.maxHealth)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@@ -148,8 +145,9 @@ public class PlanDataExtension implements DataExtension {
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getHunger(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> (long) versionedUserData.userData().getStatusData().hunger)
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (long) statusData.hunger)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@@ -163,8 +161,9 @@ public class PlanDataExtension implements DataExtension {
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getExperienceLevel(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> (long) versionedUserData.userData().getStatusData().expLevel)
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(statusData -> (long) statusData.expLevel)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@@ -178,8 +177,9 @@ public class PlanDataExtension implements DataExtension {
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public String getGameMode(@NotNull UUID uuid) {
|
||||
return getCurrentUserData(uuid).join().map(
|
||||
versionedUserData -> versionedUserData.userData().getStatusData().gameMode.toLowerCase())
|
||||
return getCurrentUserData(uuid).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getStatus())
|
||||
.map(status -> status.gameMode)
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
@@ -192,8 +192,9 @@ public class PlanDataExtension implements DataExtension {
|
||||
)
|
||||
@Tab("Current Status")
|
||||
public long getAdvancementsCompleted(@NotNull UUID playerUUID) {
|
||||
return getCurrentUserData(playerUUID).join().map(
|
||||
versionedUserData -> (long) versionedUserData.userData().getAdvancementData().size())
|
||||
return getCurrentUserData(playerUUID).join()
|
||||
.flatMap(versionedUserData -> versionedUserData.userData().getAdvancements())
|
||||
.map(advancementsData -> (long) advancementsData.size())
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
@@ -201,7 +202,7 @@ public class PlanDataExtension implements DataExtension {
|
||||
@TableProvider(tableColor = Color.LIGHT_BLUE)
|
||||
@Tab("Data Snapshots")
|
||||
public Table getDataSnapshots(@NotNull UUID playerUUID) {
|
||||
Table.Factory dataSnapshotsTable = Table.builder()
|
||||
final Table.Factory dataSnapshotsTable = Table.builder()
|
||||
.columnOne("Time", new Icon(Family.SOLID, "clock", Color.NONE))
|
||||
.columnOneFormat(TableColumnFormat.DATE_SECOND)
|
||||
.columnTwo("ID", new Icon(Family.SOLID, "bolt", Color.NONE))
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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.data.ItemData;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.editor.ItemEditorMenuType;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
@@ -57,7 +55,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 {
|
||||
@@ -152,9 +150,10 @@ public abstract class EventListener {
|
||||
// Handle asynchronous disconnection
|
||||
lockedPlayers.add(user.uuid);
|
||||
CompletableFuture.runAsync(() -> plugin.getRedisManager().setUserServerSwitch(user)
|
||||
.thenRun(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData ->
|
||||
optionalUserData.ifPresent(userData -> plugin.getRedisManager().setUserData(user, userData)
|
||||
.thenRun(() -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT)))))
|
||||
.thenRun(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).thenAccept(
|
||||
optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager()
|
||||
.setUserData(user, userData).thenRun(() -> plugin.getDatabase()
|
||||
.setUserData(user, userData, DataSaveCause.DISCONNECT)))))
|
||||
.thenRun(() -> lockedPlayers.remove(user.uuid)).exceptionally(throwable -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"An exception occurred handling a player disconnection");
|
||||
@@ -164,40 +163,34 @@ public abstract class EventListener {
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously handles a world save event
|
||||
* Handles the saving of data when the world save event is fired
|
||||
*
|
||||
* @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)) {
|
||||
protected final void saveOnWorldSave(@NotNull List<OnlineUser> usersInWorld) {
|
||||
if (disabling || !plugin.getSettings().saveOnWorldSave) {
|
||||
return;
|
||||
}
|
||||
usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter()).join().ifPresent(
|
||||
usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.WORLD_SAVE).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an inventory menu closing
|
||||
* Handles the saving of data when a player dies
|
||||
*
|
||||
* @param user The user who closed the menu
|
||||
* @param menuInventory Serialized {@link ItemData} containing the inventory contents
|
||||
* @implNote The size of the serialized {@link ItemData} array is determined by the {@link ItemEditorMenuType} of the closed inventory
|
||||
* @param user The user who died
|
||||
* @param drops The items that this user would have dropped
|
||||
*/
|
||||
protected final void handleMenuClose(@NotNull OnlineUser user, @NotNull ItemData menuInventory) {
|
||||
if (disabling) {
|
||||
protected void saveOnPlayerDeath(@NotNull OnlineUser user, @NotNull ItemData drops) {
|
||||
if (disabling || !plugin.getSettings().saveOnDeath) {
|
||||
return;
|
||||
}
|
||||
plugin.getDataEditor().closeInventoryMenu(user, menuInventory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an inventory click should be cancelled
|
||||
*
|
||||
* @param user {@link OnlineUser} performing the event
|
||||
* @return Whether the event should be cancelled
|
||||
*/
|
||||
protected final boolean cancelInventoryClick(@NotNull OnlineUser user) {
|
||||
return plugin.getDataEditor().cancelMenuEdit(user) || cancelPlayerEvent(user);
|
||||
user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings())
|
||||
.thenAccept(data -> data.ifPresent(userData -> {
|
||||
userData.getInventory().orElse(ItemData.empty()).serializedItems = drops.serializedItems;
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.DEATH);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,7 +210,7 @@ public abstract class EventListener {
|
||||
disabling = true;
|
||||
|
||||
plugin.getOnlineUsers().stream().filter(user -> !lockedPlayers.contains(user.uuid)).forEach(
|
||||
user -> user.getUserData(plugin.getLoggingAdapter()).join().ifPresent(
|
||||
user -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join().ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join()));
|
||||
|
||||
plugin.getDatabase().close();
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import de.themoep.minedown.adventure.MineDown;
|
||||
import net.william278.desertwell.Version;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.*;
|
||||
import net.william278.husksync.editor.ItemEditorMenu;
|
||||
import net.william278.husksync.event.EventCannon;
|
||||
import net.william278.husksync.event.PreSyncEvent;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import net.william278.husksync.util.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -39,9 +38,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
|
||||
@@ -164,73 +184,6 @@ public abstract class OnlineUser extends User {
|
||||
@NotNull
|
||||
public abstract Version getMinecraftVersion();
|
||||
|
||||
/**
|
||||
* Set {@link UserData} to a player
|
||||
*
|
||||
* @param data The data to set
|
||||
* @param settings Plugin settings, for determining what needs setting
|
||||
* @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}
|
||||
*/
|
||||
public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull Settings settings,
|
||||
@NotNull EventCannon eventCannon, @NotNull Logger logger,
|
||||
@NotNull Version serverMinecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Prevent synchronising user data from newer versions of Minecraft
|
||||
if (Version.minecraftVersion(data.getMinecraftVersion()).compareTo(serverMinecraftVersion) > 0) {
|
||||
logger.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the Minecraft version of their user data (" + data.getMinecraftVersion() +
|
||||
") is newer than the server's Minecraft version (" + serverMinecraftVersion + ").");
|
||||
return false;
|
||||
}
|
||||
// Prevent synchronising user data from newer versions of the plugin
|
||||
if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) {
|
||||
logger.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the format version of their user data (v" + data.getFormatVersion() +
|
||||
") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ").");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fire the PreSyncEvent
|
||||
final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join();
|
||||
final UserData finalData = preSyncEvent.getUserData();
|
||||
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
|
||||
if (!isOffline() && !preSyncEvent.isCancelled()) {
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
|
||||
add(setInventory(finalData.getInventoryData()));
|
||||
}
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ENDER_CHESTS)) {
|
||||
add(setEnderChest(finalData.getEnderChestData()));
|
||||
}
|
||||
add(setStatus(finalData.getStatusData(), StatusDataFlag.getFromSettings(settings)));
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_POTION_EFFECTS)) {
|
||||
add(setPotionEffects(finalData.getPotionEffectsData()));
|
||||
}
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_ADVANCEMENTS)) {
|
||||
add(setAdvancements(finalData.getAdvancementData()));
|
||||
}
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_STATISTICS)) {
|
||||
add(setStatistics(finalData.getStatisticsData()));
|
||||
}
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_LOCATION)) {
|
||||
add(setLocation(finalData.getLocationData()));
|
||||
}
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_PERSISTENT_DATA_CONTAINER)) {
|
||||
add(setPersistentDataContainer(finalData.getPersistentDataContainerData()));
|
||||
}
|
||||
}
|
||||
}};
|
||||
// Apply operations in parallel, join when complete
|
||||
return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true)
|
||||
.exceptionally(exception -> {
|
||||
// Handle synchronisation exceptions
|
||||
logger.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
return false;
|
||||
}).join();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a MineDown-formatted message to this player
|
||||
*
|
||||
@@ -254,25 +207,151 @@ public abstract class OnlineUser extends User {
|
||||
public abstract boolean hasPermission(@NotNull String node);
|
||||
|
||||
/**
|
||||
* Show the player a {@link ItemEditorMenu} GUI
|
||||
* Show a GUI chest menu to the player, containing the given {@link ItemData}
|
||||
*
|
||||
* @param menu The {@link ItemEditorMenu} interface to show
|
||||
* @param itemData Item data to be shown in the GUI
|
||||
* @param editable If the player should be able to remove, replace and move around the items
|
||||
* @param minimumRows The minimum number of rows to show in the chest menu
|
||||
* @param title The title of the chest menu, as a {@link MineDown} locale
|
||||
* @return A future returning the {@link ItemData} in the chest menu when the player closes it
|
||||
* @since 2.1
|
||||
*/
|
||||
public abstract void showMenu(@NotNull ItemEditorMenu menu);
|
||||
public abstract CompletableFuture<Optional<ItemData>> showMenu(@NotNull ItemData itemData, boolean editable,
|
||||
int minimumRows, @NotNull MineDown title);
|
||||
|
||||
/**
|
||||
* Get the player's current {@link UserData} in an {@link Optional}
|
||||
* </p>
|
||||
* Returns true if the player is dead
|
||||
*
|
||||
* @return true if the player is dead
|
||||
*/
|
||||
public abstract boolean isDead();
|
||||
|
||||
/**
|
||||
* Apply {@link UserData} to a player, updating their inventory, status, statistics, etc. as per the config.
|
||||
* <p>
|
||||
* This will only set data that is enabled as per the enabled settings in the config file.
|
||||
* Data present in the {@link UserData} object, but not enabled to be set in the config, will be ignored.
|
||||
*
|
||||
* @param data The {@link UserData} to set to the player
|
||||
* @param settings The plugin {@link Settings} to determine which data to set
|
||||
* @param eventCannon The {@link EventCannon} to fire the synchronisation events
|
||||
* @param logger The {@link Logger} for debug and error logging
|
||||
* @param serverMinecraftVersion The server's Minecraft version, for validating the format of the {@link UserData}
|
||||
* @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}.
|
||||
*/
|
||||
public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull Settings settings,
|
||||
@NotNull EventCannon eventCannon, @NotNull Logger logger,
|
||||
@NotNull Version serverMinecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Prevent synchronising user data from newer versions of Minecraft
|
||||
if (Version.fromMinecraftVersionString(data.getMinecraftVersion()).compareTo(serverMinecraftVersion) > 0) {
|
||||
logger.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the Minecraft version of their user data (" + data.getMinecraftVersion() +
|
||||
") is newer than the server's Minecraft version (" + serverMinecraftVersion + ").");
|
||||
return false;
|
||||
}
|
||||
// Prevent synchronising user data from newer versions of the plugin
|
||||
if (data.getFormatVersion() > UserData.CURRENT_FORMAT_VERSION) {
|
||||
logger.log(Level.SEVERE, "Cannot set data for " + username +
|
||||
" because the format version of their user data (v" + data.getFormatVersion() +
|
||||
") is newer than the current format version (v" + UserData.CURRENT_FORMAT_VERSION + ").");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fire the PreSyncEvent
|
||||
final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join();
|
||||
final UserData finalData = preSyncEvent.getUserData();
|
||||
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
|
||||
if (!isOffline() && !preSyncEvent.isCancelled()) {
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.INVENTORIES)) {
|
||||
finalData.getInventory().ifPresent(itemData -> add(setInventory(itemData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
|
||||
finalData.getEnderChest().ifPresent(itemData -> add(setEnderChest(itemData)));
|
||||
}
|
||||
finalData.getStatus().ifPresent(statusData -> add(setStatus(statusData, settings)));
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
|
||||
finalData.getPotionEffects().ifPresent(potionEffectData -> add(setPotionEffects(potionEffectData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
|
||||
finalData.getAdvancements().ifPresent(advancementData -> add(setAdvancements(advancementData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
|
||||
finalData.getStatistics().ifPresent(statisticData -> add(setStatistics(statisticData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
|
||||
finalData.getLocation().ifPresent(locationData -> add(setLocation(locationData)));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
|
||||
finalData.getPersistentDataContainer().ifPresent(persistentDataContainerData ->
|
||||
add(setPersistentDataContainer(persistentDataContainerData)));
|
||||
}
|
||||
}
|
||||
}};
|
||||
// Apply operations in parallel, join when complete
|
||||
return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true)
|
||||
.exceptionally(exception -> {
|
||||
// Handle synchronisation exceptions
|
||||
logger.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
return false;
|
||||
}).join();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the player's current {@link UserData} in an {@link Optional}.
|
||||
* <p>
|
||||
* Since v2.1, this method will respect the data synchronisation settings; user data will only be as big as the
|
||||
* enabled synchronisation values set in the config file
|
||||
* <p>
|
||||
* Also note that if the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set,
|
||||
* the user's inventory will only be returned if the player is alive.
|
||||
* <p>
|
||||
* If the user data could not be returned due to an exception, the optional will return empty
|
||||
*
|
||||
* @param logger The logger to use for handling exceptions
|
||||
* @return the player's current {@link UserData} in an optional; empty if an exception occurs
|
||||
*/
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger) {
|
||||
return CompletableFuture.supplyAsync(() -> Optional.of(new UserData(getStatus().join(), getInventory().join(),
|
||||
getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(),
|
||||
getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(),
|
||||
getMinecraftVersion().toString())))
|
||||
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger, @NotNull Settings settings) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
final UserDataBuilder builder = UserData.builder(getMinecraftVersion());
|
||||
final List<CompletableFuture<Void>> dataGetOperations = new ArrayList<>() {{
|
||||
if (!isOffline()) {
|
||||
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.getSynchronizationFeature(Settings.SynchronizationFeature.ENDER_CHESTS)) {
|
||||
add(getEnderChest().thenAccept(builder::setEnderChest));
|
||||
}
|
||||
add(getStatus().thenAccept(builder::setStatus));
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.POTION_EFFECTS)) {
|
||||
add(getPotionEffects().thenAccept(builder::setPotionEffects));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.ADVANCEMENTS)) {
|
||||
add(getAdvancements().thenAccept(builder::setAdvancements));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.STATISTICS)) {
|
||||
add(getStatistics().thenAccept(builder::setStatistics));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.LOCATION)) {
|
||||
add(getLocation().thenAccept(builder::setLocation));
|
||||
}
|
||||
if (settings.getSynchronizationFeature(Settings.SynchronizationFeature.PERSISTENT_DATA_CONTAINER)) {
|
||||
add(getPersistentDataContainer().thenAccept(builder::setPersistentDataContainer));
|
||||
}
|
||||
}
|
||||
}};
|
||||
|
||||
// Apply operations in parallel, join when complete
|
||||
CompletableFuture.allOf(dataGetOperations.toArray(new CompletableFuture[0])).join();
|
||||
return Optional.of(builder.build());
|
||||
})
|
||||
.exceptionally(exception -> {
|
||||
logger.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
|
||||
@@ -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();
|
||||
@@ -130,6 +129,8 @@ public class RedisManager {
|
||||
jedis.setex(getKey(RedisKeyType.DATA_UPDATE, user.uuid),
|
||||
RedisKeyType.DATA_UPDATE.timeToLive,
|
||||
plugin.getDataAdapter().toBytes(userData));
|
||||
|
||||
// Debug logging
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Set " + RedisKeyType.DATA_UPDATE.name()
|
||||
+ " key to redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Utility class for dumping {@link UserDataSnapshot}s to a file or as a paste on the web
|
||||
*/
|
||||
public class DataDumper {
|
||||
|
||||
private static final String LOGS_SITE_ENDPOINT = "https://api.mclo.gs/1/log";
|
||||
|
||||
private final HuskSync plugin;
|
||||
private final UserDataSnapshot dataSnapshot;
|
||||
private final User user;
|
||||
|
||||
private DataDumper(@NotNull UserDataSnapshot dataSnapshot,
|
||||
@NotNull User user, @NotNull HuskSync implementor) {
|
||||
this.dataSnapshot = dataSnapshot;
|
||||
this.user = user;
|
||||
this.plugin = implementor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@link DataDumper} of the given {@link UserDataSnapshot}
|
||||
*
|
||||
* @param dataSnapshot The {@link UserDataSnapshot} to dump
|
||||
* @param user The {@link User} whose data is being dumped
|
||||
* @param plugin The implementing {@link HuskSync} plugin
|
||||
* @return A {@link DataDumper} for the given {@link UserDataSnapshot}
|
||||
*/
|
||||
public static DataDumper create(@NotNull UserDataSnapshot dataSnapshot,
|
||||
@NotNull User user, @NotNull HuskSync plugin) {
|
||||
return new DataDumper(dataSnapshot, user, plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dumps the data snapshot to a string
|
||||
*
|
||||
* @return the data snapshot as a string
|
||||
*/
|
||||
@Override
|
||||
@NotNull
|
||||
public String toString() {
|
||||
return plugin.getDataAdapter().toJson(dataSnapshot.userData(), true);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String toWeb() {
|
||||
try {
|
||||
final URL url = new URL(LOGS_SITE_ENDPOINT);
|
||||
final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("POST");
|
||||
connection.setDoOutput(true);
|
||||
|
||||
// Dispatch the request
|
||||
final byte[] messageBody = getWebContentField().getBytes(StandardCharsets.UTF_8);
|
||||
final int messageLength = messageBody.length;
|
||||
connection.setFixedLengthStreamingMode(messageLength);
|
||||
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
connection.connect();
|
||||
try (OutputStream messageOutputStream = connection.getOutputStream()) {
|
||||
messageOutputStream.write(messageBody);
|
||||
}
|
||||
|
||||
// Get the response
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
// Get the body as a json
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
final StringBuilder response = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
response.append(line);
|
||||
}
|
||||
|
||||
// Parse the response as json
|
||||
final JsonObject responseJson = JsonParser.parseString(response.toString()).getAsJsonObject();
|
||||
if (responseJson.has("url")) {
|
||||
return responseJson.get("url").getAsString();
|
||||
}
|
||||
return "(Failed to get URL from response)";
|
||||
}
|
||||
} else {
|
||||
return "(Failed to upload to logs site, got: " + connection.getResponseCode() + ")";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to upload data to logs site", e);
|
||||
}
|
||||
return "(Failed to upload to logs site)";
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getWebContentField() {
|
||||
return "content=" + URLEncoder.encode(toString(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump the {@link UserDataSnapshot} to a file and return the file name
|
||||
*
|
||||
* @return the relative path of the file the data was dumped to
|
||||
*/
|
||||
@NotNull
|
||||
public String toFile() throws IOException {
|
||||
final File filePath = getFilePath();
|
||||
|
||||
// Write the data from #getString to the file using a writer
|
||||
try (final FileWriter writer = new FileWriter(filePath, StandardCharsets.UTF_8, false)) {
|
||||
writer.write(toString());
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Failed to write data to file", e);
|
||||
}
|
||||
|
||||
return "~/plugins/HuskSync/dumps/" + filePath.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path to dump the data to
|
||||
*
|
||||
* @return the file path
|
||||
* @throws IOException if the prerequisite dumps parent folder could not be created
|
||||
*/
|
||||
@NotNull
|
||||
private File getFilePath() throws IOException {
|
||||
return new File(getDumpsFolder(), getFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the folder to dump the data to and create it if it does not exist
|
||||
*
|
||||
* @return the dumps folder
|
||||
* @throws IOException if the folder could not be created
|
||||
*/
|
||||
@NotNull
|
||||
private File getDumpsFolder() throws IOException {
|
||||
final File dumpsFolder = new File(plugin.getDataFolder(), "dumps");
|
||||
if (!dumpsFolder.exists()) {
|
||||
if (!dumpsFolder.mkdirs()) {
|
||||
throw new IOException("Failed to create user data dumps folder");
|
||||
}
|
||||
}
|
||||
return dumpsFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the file to dump the data snapshot to
|
||||
*
|
||||
* @return the file name
|
||||
*/
|
||||
@NotNull
|
||||
private String getFileName() {
|
||||
return new StringJoiner("_")
|
||||
.add(user.username)
|
||||
.add(new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(dataSnapshot.versionTimestamp()))
|
||||
.add(dataSnapshot.cause().name().toLowerCase())
|
||||
.add(dataSnapshot.versionUUID().toString().split("-")[0])
|
||||
+ ".json";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.data.UserDataSnapshot;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
import net.william278.husksync.player.User;
|
||||
import net.william278.paginedown.PaginatedList;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Represents a chat-viewable paginated list of {@link UserDataSnapshot}s
|
||||
*/
|
||||
public class DataSnapshotList {
|
||||
|
||||
// Used for displaying number ordering next to snapshots in the list
|
||||
private static final String[] CIRCLED_NUMBER_ICONS = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
|
||||
|
||||
@NotNull
|
||||
private final PaginatedList paginatedList;
|
||||
|
||||
private DataSnapshotList(@NotNull List<UserDataSnapshot> snapshots, @NotNull User dataOwner,
|
||||
@NotNull Locales locales) {
|
||||
final AtomicInteger snapshotNumber = new AtomicInteger(1);
|
||||
this.paginatedList = PaginatedList.of(snapshots.stream()
|
||||
.map(snapshot -> locales.getRawLocale("data_list_item",
|
||||
getNumberIcon(snapshotNumber.getAndIncrement()),
|
||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss")
|
||||
.format(snapshot.versionTimestamp()),
|
||||
snapshot.versionUUID().toString().split("-")[0],
|
||||
snapshot.versionUUID().toString(),
|
||||
snapshot.cause().name().toLowerCase().replaceAll("_", " "),
|
||||
dataOwner.username,
|
||||
snapshot.pinned() ? "※" : " ")
|
||||
.orElse("• " + snapshot.versionUUID())).toList(),
|
||||
locales.getBaseChatList(6)
|
||||
.setHeaderFormat(locales.getRawLocale("data_list_title", dataOwner.username,
|
||||
"%first_item_on_page_index%", "%last_item_on_page_index%", "%total_items%")
|
||||
.orElse(""))
|
||||
.setCommand("/husksync:userdata list " + dataOwner.username)
|
||||
.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link DataSnapshotList} from a list of {@link UserDataSnapshot}s
|
||||
*
|
||||
* @param snapshots The list of {@link UserDataSnapshot}s to display
|
||||
* @param user The {@link User} who owns the {@link UserDataSnapshot}s
|
||||
* @param locales The {@link Locales} instance
|
||||
* @return A new {@link DataSnapshotList}, to be viewed with {@link #displayPage(OnlineUser, int)}
|
||||
*/
|
||||
public static DataSnapshotList create(@NotNull List<UserDataSnapshot> snapshots, @NotNull User user,
|
||||
@NotNull Locales locales) {
|
||||
return new DataSnapshotList(snapshots, user, locales);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an icon for the given snapshot number, via {@link #CIRCLED_NUMBER_ICONS}
|
||||
*
|
||||
* @param number the snapshot number
|
||||
* @return the icon for the given snapshot number
|
||||
*/
|
||||
private static String getNumberIcon(int number) {
|
||||
if (number < 1 || number > 20) {
|
||||
return String.valueOf(number);
|
||||
}
|
||||
return CIRCLED_NUMBER_ICONS[number - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a page of the list of {@link UserDataSnapshot} to the user
|
||||
*
|
||||
* @param onlineUser The online user to display the message to
|
||||
* @param page The page number to display
|
||||
*/
|
||||
public void displayPage(@NotNull OnlineUser onlineUser, int page) {
|
||||
onlineUser.sendMessage(paginatedList.getNearestValidPage(page));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.logging.Level;
|
||||
@@ -16,8 +15,6 @@ public abstract class Logger {
|
||||
|
||||
public abstract void log(@NotNull Level level, @NotNull String message);
|
||||
|
||||
public abstract void log(@NotNull Level level, @NotNull MineDown mineDown);
|
||||
|
||||
public abstract void info(@NotNull String message);
|
||||
|
||||
public abstract void severe(@NotNull String message);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
@@ -14,15 +13,8 @@ public interface ResourceReader {
|
||||
* Gets the resource with given filename and reads it as an {@link InputStream}
|
||||
*
|
||||
* @param fileName Name of the resource file to read
|
||||
* @return The resource, read as an {@link InputStream}
|
||||
* @return The resource, read as an {@link InputStream}; or {@code null} if the resource was not found
|
||||
*/
|
||||
@NotNull InputStream getResource(String fileName);
|
||||
|
||||
/**
|
||||
* Gets the plugin data folder where plugin configuration and data are kept
|
||||
*
|
||||
* @return the plugin data directory
|
||||
*/
|
||||
@NotNull File getDataFolder();
|
||||
@Nullable InputStream getResource(String fileName);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class UpdateChecker {
|
||||
|
||||
private final static int SPIGOT_PROJECT_ID = 97144;
|
||||
private final Logger logger;
|
||||
private final Version currentVersion;
|
||||
|
||||
public UpdateChecker(@NotNull Version currentVersion, @NotNull Logger logger) {
|
||||
this.currentVersion = currentVersion;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
public CompletableFuture<Version> fetchLatestVersion() {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try {
|
||||
final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
|
||||
URLConnection urlConnection = url.openConnection();
|
||||
return Version.pluginVersion(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
|
||||
} catch (Exception e) {
|
||||
logger.log(Level.WARNING, "Failed to fetch the latest plugin version", e);
|
||||
}
|
||||
return new Version();
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isUpdateAvailable(@NotNull Version latestVersion) {
|
||||
return latestVersion.compareTo(currentVersion) > 0;
|
||||
}
|
||||
|
||||
public Version getCurrentVersion() {
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
public CompletableFuture<Boolean> isUpToDate() {
|
||||
return fetchLatestVersion().thenApply(this::isUpdateAvailable);
|
||||
}
|
||||
|
||||
public void logToConsole() {
|
||||
fetchLatestVersion().thenAccept(latestVersion -> {
|
||||
if (isUpdateAvailable(latestVersion)) {
|
||||
logger.log(Level.WARNING, "A new version of HuskSync is available: v" + latestVersion);
|
||||
} else {
|
||||
logger.log(Level.INFO, "HuskSync is up-to-date! (Running: v" + getCurrentVersion().toString() + ")");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package net.william278.husksync.util;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class Version implements Comparable<Version> {
|
||||
private final static String VERSION_SEPARATOR = ".";
|
||||
private final static String MINECRAFT_META_SEPARATOR = "-";
|
||||
private final static String PLUGIN_META_SEPARATOR = "+";
|
||||
|
||||
private int[] versions = new int[]{};
|
||||
@NotNull
|
||||
private String metadata = "";
|
||||
@NotNull
|
||||
private String metaSeparator = "";
|
||||
|
||||
protected Version() {
|
||||
}
|
||||
|
||||
private Version(@NotNull String version, @NotNull String metaSeparator) {
|
||||
this.parse(version, metaSeparator);
|
||||
this.metaSeparator = metaSeparator;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Version pluginVersion(@NotNull String versionString) {
|
||||
return new Version(versionString, PLUGIN_META_SEPARATOR);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static Version minecraftVersion(@NotNull String versionString) {
|
||||
return new Version(versionString, MINECRAFT_META_SEPARATOR);
|
||||
}
|
||||
|
||||
private void parse(@NotNull String version, @NotNull String metaSeparator) {
|
||||
int metaIndex = version.indexOf(metaSeparator);
|
||||
if (metaIndex > 0) {
|
||||
this.metadata = version.substring(metaIndex + 1);
|
||||
version = version.substring(0, metaIndex);
|
||||
}
|
||||
String[] versions = version.split(Pattern.quote(VERSION_SEPARATOR));
|
||||
this.versions = Arrays.stream(versions).mapToInt(Integer::parseInt).toArray();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(@NotNull Version other) {
|
||||
int length = Math.max(this.versions.length, other.versions.length);
|
||||
for (int i = 0; i < length; i++) {
|
||||
int a = i < this.versions.length ? this.versions[i] : 0;
|
||||
int b = i < other.versions.length ? other.versions[i] : 0;
|
||||
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringJoiner joiner = new StringJoiner(VERSION_SEPARATOR);
|
||||
for (int version : this.versions) {
|
||||
joiner.add(String.valueOf(version));
|
||||
}
|
||||
return joiner + ((!this.metadata.isEmpty()) ? (this.metaSeparator + this.metadata) : "");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
# ------------------------------
|
||||
# | HuskSync Config |
|
||||
# | Developed by William278 |
|
||||
# ------------------------------
|
||||
# Documentation available at: https://william278.net/docs/husksync/Setup
|
||||
|
||||
language: 'en-gb'
|
||||
check_for_updates: true
|
||||
cluster_id: ''
|
||||
debug_logging: false
|
||||
|
||||
database:
|
||||
credentials:
|
||||
host: 'localhost'
|
||||
port: 3306
|
||||
database: 'HuskSync'
|
||||
username: 'root'
|
||||
password: 'pa55w0rd'
|
||||
params: '?autoReconnect=true&useSSL=false'
|
||||
connection_pool:
|
||||
maximum_pool_size: 10
|
||||
minimum_idle: 10
|
||||
maximum_lifetime: 1800000
|
||||
keepalive_time: 0
|
||||
connection_timeout: 5000
|
||||
table_names:
|
||||
users_table: 'husksync_users'
|
||||
user_data_table: 'husksync_user_data'
|
||||
|
||||
redis:
|
||||
credentials:
|
||||
host: 'localhost'
|
||||
port: 6379
|
||||
password: ''
|
||||
use_ssl: false
|
||||
|
||||
synchronization:
|
||||
max_user_data_snapshots: 5
|
||||
save_on_world_save: true
|
||||
compress_data: true
|
||||
network_latency_milliseconds: 500
|
||||
features:
|
||||
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: true
|
||||
location: false
|
||||
|
||||
config_version: 1
|
||||
41
common/src/main/resources/locales/bg-bg.yml
Normal file
41
common/src/main/resources/locales/bg-bg.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
synchronisation_complete: '[⏵ Данните синхронизирани!](#00fb9a)'
|
||||
synchronisation_failed: '[⏵ Провалихме се да синхронизираме Вашите данни! Моля свържете се с администратор.](#ff7e5e)'
|
||||
reload_complete: '[HuskSync](#00fb9a bold) [| Презаредихме конфигурацията и файловете със съобщения.](#00fb9a)'
|
||||
error_invalid_syntax: '[Грешка:](#ff3300) [Неправилен синтаксис. Използвайте: %1%](#ff7e5e)'
|
||||
error_invalid_player: '[Грешка:](#ff3300) [Не можахме да открием играч с това име.](#ff7e5e)'
|
||||
error_no_permission: '[Грешка:](#ff3300) [Нямате право да използвате тази команда](#ff7e5e)'
|
||||
error_console_command_only: '[Грешка:](#ff3300) [Тази команда може да бъде използвана единствено през конзолата](#ff7e5e)'
|
||||
error_in_game_command_only: 'Грешка: Тази команда може да бъде използвана само от играта.'
|
||||
error_no_data_to_display: '[Грешка:](#ff3300) [Не можахме да открием никакви данни за потребителя, които да покажем.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[Грешка:](#ff3300) [Не можахме да открием никакви потребителски данни за тази версия на това UUID.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0Инвентара на %1%'
|
||||
ender_chest_viewer_menu_title: '&0Ендър Сандъка на %1%'
|
||||
inventory_viewer_opened: '[Преглеждане снапшота на](#00fb9a) [%1%](#00fb9a bold) [ инвентар от ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Преглеждане снапшота на](#00fb9a) [%1%](#00fb9a bold) [ Ендър Сандък от ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Вашите данни бяха обновени!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Провалихме се да обновим Вашите данни! Моля свържете се с администратор.](#ff7e5e)'
|
||||
data_manager_title: '[Преглеждане потребителският снапшот](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%](#00fb9a bold show_text=&7UUID на Играча:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Клеймо на Версията:\n&8Когато данните са били запазени)'
|
||||
data_manager_pinned: '[※ Закачен снапшот](#d8ff2b show_text=&7Закачен:\n&8Снапшота на този потребител няма да бъде автоматично завъртан.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Точки кръв) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Точки глад) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Ниво опит) [🏹 %5%](dark_aqua show_text=&7Режим на игра)'
|
||||
data_manager_advancements_statistics: '[⭐ Напредъци: %1%](color=#ffc43b-#f5c962 show_text=&7Напредъци, в които имате прогрес:\n&8%2%) [⌛ Изиграно Време: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Изиграно време в играта\n&8⚠ Базирано на статистики от играта)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Инвентар…]](color=#a17b5f-#f5b98c show_text=&7Натиснете, за да прегледате run_command=/inventory %1% %2%) [[⌀ Ендър Сандък…]](#b649c4-#d254ff show_text=&7Натиснете, за да проверите run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Управление:](gray) [[❌ Изтрий…]](#ff3300 show_text=&7Натиснете, за да изтриете този снапшот от потребителски данни.\n&8Това няма да засегне текущите данни на потребителя.\n&#ff3300&⚠ Това не може да бъде отменено! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Възстанови…]](#00fb9a show_text=&7Натиснете, за да възстановите тези потребителски данни.\n&8Това ще зададе данните на потребителя на този снапшот.\n&#ff3300&⚠ Текущите данни на %1% ще бъдат пренаписани! suggest_command=/husksync:userdata restore %1% %2%) [[※ Закачи/Откачи…]](#d8ff2b show_text=&7Натиснете, за да закачите или откачите този снапшот с потребителски данни\n&8Закачените снапшоти няма да бъдат автоматично завъртани run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7и още %1%…'
|
||||
data_list_title: '[Лист от](#00fb9a) [снапшоти на данните на потребителя](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Снапшот с данни %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Закачен:\n&8Закачените снапшоти няма да бъдат автоматично завъртани. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Клеймо на Версията:&7\n&8Когато данните са били запазени run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Версия на UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Причина на Запазване:\n&8Какво е накарало данните да бъдат запазени run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Успешно изтрихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_restored: '[⏪ Успешно възстановихме](#00fb9a) [текущите потребителски данни за](#00fb9a) [%1%](#00fb9a show_text=&7UUID на Играча:\n&8%2%) [от снапшот](#00fb9a) [%3%.](#00fb9a show_text=&7Версия на UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Успешно закачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_unpinned: '[※ Успешно откачихме снапшота с потребителски данни](#00fb9a) [%1%](#00fb9a show_text=&7Версия на UUID:\n&8%2%) [за](#00fb9a) [%3%.](#00fb9a show_text=&7UUID на Играча:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -14,18 +14,28 @@ inventory_viewer_opened: '[Du siehst den Schnappschuss des Inventares von](#00fb
|
||||
ender_chest_viewer_opened: '[Du siehst den Schnappschuss der Endertruhe von](#00fb9a) [%1%](#00fb9a bold) [von ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Deine Daten wurden aktualisiert!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Ein Fehler ist beim Aktualisieren deiner Daten aufgetreten! Bitte kontaktiere einen Administrator.](#ff7e5e)'
|
||||
data_manager_title: '[Du siehst den Nutzerdaten-Schnappschuss](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\\n&8%2%) [für [%3%](#00fb9a bold show_text=&7Spieler-UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:\\n&8Zeitpunkt der Speicherung der Daten)'
|
||||
data_manager_pinned: '[※ Schnappschuss angeheftet](#d8ff2b show_text=&7Angeheftet:\\n&8Dieser Nutzerdaten-Schnappschuss wird nicht automatisch rotiert.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Speicherungsgrund:\\n&8Der Grund für das Speichern der Daten)\\n'
|
||||
data_manager_title: '[Du siehst den Nutzerdaten-Schnappschuss](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für [%3%](#00fb9a bold show_text=&7Spieler-UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:\n&8Zeitpunkt der Speicherung der Daten)'
|
||||
data_manager_pinned: '[※ Schnappschuss angeheftet](#d8ff2b show_text=&7Angeheftet:\n&8Dieser Nutzerdaten-Schnappschuss wird nicht automatisch rotiert.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Lebenspunkte) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hungerpunkte) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP-Level) [🏹 %5%](dark_aqua show_text=&7Spielmodus)'
|
||||
data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\\n&8⚠ Basierend auf Spielstatistiken)'
|
||||
data_manager_item_buttons: '[[🪣 Inventar…]](color=#a17b5f-#f5b98c show_text=&7Klicke zum Ansehen run_command=/inventory %1% %2%) [[⌀ Endertruhe…]](#b649c4-#d254ff show_text=&7Klicke zum Ansehen run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Verwalte:](gray) [[❌ Löschen…]](#ff3300 show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss zu löschen.\\n&8Dies betrifft nicht die aktuellen Nutzerdaten.\\n&#ff3300&⚠ Dieser Schritt kann nicht rückgängig gemacht werden! run_command=/userdata delete %1% %2%) [[⏪ Wiederherstellen…]](#00fb9a show_text=&7Klicke, um die Nutzerdaten wiederherzustellen.\\n&8Dies wird die Nutzerdaten auf den Stand des Schnappschusses setzen.\\n&#ff3300&⚠ Die aktuellen Nutzerdaten von %1% werden überschrieben! run_command=/userdata restore %1% %2%) [[※ Anheften/Loslösen…]](#d8ff2b show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss anzuheften oder loszulösen\\n&8Angeheftete Nutzerdaten-Schnappschüsse werden nicht automatisch rotiert run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Erfolge: %1%](color=#ffc43b-#f5c962 show_text=&7Erfolge in denen du Fortschritt gemacht hast:\n&8%2%) [⌛ Spielzeit: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Deine verbrachte Zeit im Spiel\n&8⚠ Basierend auf Spielstatistiken)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventar…]](color=#a17b5f-#f5b98c show_text=&7Klicke zum Ansehen run_command=/inventory %1% %2%) [[⌀ Endertruhe…]](#b649c4-#d254ff show_text=&7Klicke zum Ansehen run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Verwalte:](gray) [[❌ Löschen…]](#ff3300 show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss zu löschen.\n&8Dies betrifft nicht die aktuellen Nutzerdaten.\n&#ff3300&⚠ Dieser Schritt kann nicht rückgängig gemacht werden! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Wiederherstellen…]](#00fb9a show_text=&7Klicke, um die Nutzerdaten wiederherzustellen.\n&8Dies wird die Nutzerdaten auf den Stand des Schnappschusses setzen.\n&#ff3300&⚠ Die aktuellen Nutzerdaten von %1% werden überschrieben! suggest_command=/husksync:userdata restore %1% %2%) [[※ Anheften/Loslösen…]](#d8ff2b show_text=&7Klicke, um diesen Nutzerdaten-Schnappschuss anzuheften oder loszulösen\n&8Angeheftete Nutzerdaten-Schnappschüsse werden nicht automatisch rotiert run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7und %1% weitere…'
|
||||
data_list_title: '[Liste der Nutzerdaten-Schnappschüsse von](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Daten-Schnappschuss %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Angeheftet:\\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\\n&8Zeitpunkt der Speicherung der Daten run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versions-UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Speicherungsgrund:\\n&8Der Grund für das Speichern der Daten run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\\n&8%4%)'
|
||||
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Daten-Schnappschuss %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Angeheftet:\n&8Angeheftete Schnappschüsse werden nicht automatisch rotiert. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Versions-Zeitstempel:&7\n&8Zeitpunkt der Speicherung der Daten run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versions-UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Speicherungsgrund:\n&8Der Grund für das Speichern der Daten run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Nutzerdaten-Schnappschuss erfolgreich gelöscht](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Erfgreich wiederhergestellt](#00fb9a) [Aktuelle Nutzerdaten des Schnappschusses von %1%](#00fb9a show_text=&7Spieler-UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versions-UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Nutzerdaten-Schnappschuss erfolgreich angepinnt](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Nutzerdaten-Schnappschuss erfolgreich losgelöst](#00fb9a) [%1%](#00fb9a show_text=&7Versions-UUID:\n&8%2%) [für](#00fb9a) [%3%.](#00fb9a show_text=&7Spieler-UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -10,22 +10,32 @@ error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to di
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&8What caused the data to be saved)\\n'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\\n&8⚠ Based on in-game statistics)'
|
||||
data_manager_item_buttons: '[[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\\n&8This will not affect the user''s current data.\\n&#ff3300&⚠ This cannot be undone! run_command=/userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\\n&8This will set the user''s data to this snapshot.\\n&#ff3300&⚠ %1%''s current data will be overwritten! run_command=/userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7and %1% more…'
|
||||
data_list_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\\n&8What caused the data to be saved run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -14,18 +14,28 @@ inventory_viewer_opened: '[Viendo una snapshot de](#00fb9a) [%1%](#00fb9a bold)
|
||||
ender_chest_viewer_opened: '[Viendo una snapshot de](#00fb9a) [%1%](#00fb9a bold) [Enderchest a partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 ¡Tus datos han sido actualizados!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Error al actualizar tus datos, por favor, contacte con un administrador.](#ff7e5e)'
|
||||
data_manager_title: '[Viendo una snapshot sobre la informacion del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version del registro:\\n&8Cuando los datos se han guardado)'
|
||||
data_manager_pinned: '[※ Snapshot anclada](#d8ff2b show_text=&Anclado:\\n&8La informacion de este jugador no se rotará automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Motivo del guardado:\\n&8Lo que ha causado que se guarde)\\n'
|
||||
data_manager_title: '[Viendo una snapshot sobre la informacion del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version del registro:\n&8Cuando los datos se han guardado)'
|
||||
data_manager_pinned: '[※ Snapshot anclada](#d8ff2b show_text=&Anclado:\n&8La informacion de este jugador no se rotará automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Motivo del guardado:\n&8Lo que ha causado que se guarde)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Puntos de vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Puntos de hambre) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Nivel de exp) [🏹 %5%](dark_aqua show_text=&7Gamemode)'
|
||||
data_manager_advancements_statistics: '[⭐ Logros: %1%](color=#ffc43b-#f5c962 show_text=&7Logros que has conseguido:\\n&8%2%) [⌛ Tiempo de juego: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\\n&8⚠ Based on in-game statistics)'
|
||||
data_manager_item_buttons: '[[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Click para ver run_command=/inventory %1% %2%) [[⌀ Enderchest…]](#b649c4-#d254ff show_text=&7Click para ver run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Borrar…]](#ff3300 show_text=&7Click para borrar la snapshot del usuario.\\n&8Esto no afectará a la informacion actual del jugador.\\n&#ff3300&⚠ ¡Esto no se puede deshacer! run_command=/userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Click para restaurar la informacion de este usuario.\\n&8Esto hará que la informacion actual cambie por esta snapshot.\\n&#ff3300&⚠ %1% la informacion actual será sustituida! run_command=/userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click para anclar/desanclar esta snapshot\\n&8Las snapshot ancladas no seran rotadas automaticamente run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Logros: %1%](color=#ffc43b-#f5c962 show_text=&7Logros que has conseguido:\n&8%2%) [⌛ Tiempo de juego: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Click para ver run_command=/inventory %1% %2%) [[⌀ Enderchest…]](#b649c4-#d254ff show_text=&7Click para ver run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Borrar…]](#ff3300 show_text=&7Click para borrar la snapshot del usuario.\n&8Esto no afectará a la informacion actual del jugador.\n&#ff3300&⚠ ¡Esto no se puede deshacer! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Click para restaurar la informacion de este usuario.\n&8Esto hará que la informacion actual cambie por esta snapshot.\n&#ff3300&⚠ %1% la informacion actual será sustituida! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click para anclar/desanclar esta snapshot\n&8Las snapshot ancladas no seran rotadas automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7y %1% más…'
|
||||
data_list_title: '[Lista de](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[Snapshots del usuario:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Informacion de la snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Anclado:\\n&8Las snapshot ancladas no serán rotadas automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version Tiempo:&7\\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa del guardado:\\n&8Lo que sea que haya hecho que se guarde run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\\n&8%4%)'
|
||||
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Informacion de la snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Anclado:\n&8Las snapshot ancladas no serán rotadas automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version Tiempo:&7\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa del guardado:\n&8Lo que sea que haya hecho que se guarde run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Se ha eliminado correctamente la snapshot del usuario](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurado correctamente](#00fb9a) [%1%](#00fb9a show_text=&7UUID del jugador:\n&8%2%)[Informacion actual de la snapshot del jugador](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Se ha anclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
data_unpinned: '[※ Se ha desanclado perfectamente la snapshot del jugador](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7UUID del usuario:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -14,18 +14,28 @@ inventory_viewer_opened: '[Stai vedendo l''istantanea di](#00fb9a) [%1%](#00fb9a
|
||||
ender_chest_viewer_opened: '[Stai vedendo l''istantanea di](#00fb9a) [%1%](#00fb9a bold) [Ender Chest del ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 I tuoi dati sono stati aggiornati!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Aggiornamento dei tuoi dati fallito! Perfavore contatta un amministratore.](#ff7e5e)'
|
||||
data_manager_title: '[Stai vedendo l''istantanea](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\\n&8%2%) [di](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7:\\n&8Quando i dati sono stati salvati)'
|
||||
data_manager_pinned: '[※ Istantanea fissata](#d8ff2b show_text=&7Pinned:\\n&8Quest''istantanea non sarà cancellata automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&8Cosa ha causato il salvataggio dei dati)\\n'
|
||||
data_manager_title: '[Stai vedendo l''istantanea](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [di](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7:\n&8Quando i dati sono stati salvati)'
|
||||
data_manager_pinned: '[※ Istantanea fissata](#d8ff2b show_text=&7Pinned:\n&8Quest''istantanea non sarà cancellata automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8Cosa ha causato il salvataggio dei dati)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Vita) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Fame) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7Livello di XP) [🏹 %5%](dark_aqua show_text=&7Modalità di gioco)'
|
||||
data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\\n&8⚠ Basato sulle statistiche di gioco)'
|
||||
data_manager_item_buttons: '[[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Clicca per visualizzare run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clicca per visualizzare run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Gestisci:](gray) [[❌ Cancella…]](#ff3300 show_text=&7Fare clic per eliminare questa istantanea.\\n&8Questo non influisce sui dati attuali dell''utente.\\n&#ff3300&⚠ Questo non può essere annullato! run_command=/userdata delete %1% %2%) [[⏪ Ripristina…]](#00fb9a show_text=&7Clicca per ripristinare i dati dell''utente.\\n&8I dati dell''utente saranno ripristinati a quest''istantanea.\\n&#ff3300&⚠ I dati di %1% saranno sovrascritti! run_command=/userdata restore %1% %2%) [[※ fissa/sblocca...]](#d8ff2b show_text=&7Clicca per fissare o sbloccare quest''istantanea\\n&8Le istantanee fissate non saranno cancellate automaticamente run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Progressi: %1%](color=#ffc43b-#f5c962 show_text=&7Progressi compiuti in:\n&8%2%) [⌛ Tempo di gioco: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo di gioco\n&8⚠ Basato sulle statistiche di gioco)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventario…]](color=#a17b5f-#f5b98c show_text=&7Clicca per visualizzare run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clicca per visualizzare run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gestisci:](gray) [[❌ Cancella…]](#ff3300 show_text=&7Fare clic per eliminare questa istantanea.\n&8Questo non influisce sui dati attuali dell''utente.\n&#ff3300&⚠ Questo non può essere annullato! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Ripristina…]](#00fb9a show_text=&7Clicca per ripristinare i dati dell''utente.\n&8I dati dell''utente saranno ripristinati a quest''istantanea.\n&#ff3300&⚠ I dati di %1% saranno sovrascritti! suggest_command=/husksync:userdata restore %1% %2%) [[※ fissa/sblocca...]](#d8ff2b show_text=&7Clicca per fissare o sbloccare quest''istantanea\n&8Le istantanee fissate non saranno cancellate automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7e %1% altro…'
|
||||
data_list_title: '[Lista delle istantanee di](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Istantanea dei dati %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Fissato:\\n&8Le istantanee fissate non saranno cancellate automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Timestamp della versione:&7\\n&8Quando i dati sono stati salvati run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versione di UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa del salvataggio:\\n&8Cosa ha causato il salvataggio dei dati run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Istantanea dei dati %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Fissato:\n&8Le istantanee fissate non saranno cancellate automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Timestamp della versione:&7\n&8Quando i dati sono stati salvati run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Versione di UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa del salvataggio:\n&8Cosa ha causato il salvataggio dei dati run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Istantanea eliminata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Ripristato con successo](#00fb9a) [Dati dall''istantanea di](#00fb9a)[%1%](#00fb9a show_text=&7Player UUID:\n&8%2%) [%3%.](#00fb9a show_text=&7Versione di UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Instantanea fissata](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ L''istantanea dei dati utente è stata sbloccata con successo](#00fb9a) [%1%](#00fb9a show_text=&7Versione di UUID:\n&8%2%) [per](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -10,22 +10,32 @@ error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to di
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&8What caused the data to be saved)\\n'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\\n&8⚠ Based on in-game statistics)'
|
||||
data_manager_item_buttons: '[[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\\n&8This will not affect the user''s current data.\\n&#ff3300&⚠ This cannot be undone! run_command=/userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\\n&8This will set the user''s data to this snapshot.\\n&#ff3300&⚠ %1%''s current data will be overwritten! run_command=/userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7and %1% more…'
|
||||
data_list_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\\n&8What caused the data to be saved run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -14,18 +14,28 @@ inventory_viewer_opened: '[Visualizando snapshot de](#00fb9a) [%1%](#00fb9a bold
|
||||
ender_chest_viewer_opened: '[Visualizando snapshot de](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest a partir de ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Seus dados foram atualizados!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Falha na atualização de seus dados! Por favor entre em contato com um administrador.](#ff7e5e)'
|
||||
data_manager_title: '[Visualizando snapshot dos dados do usuário](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&8Quando os dados foram salvos)'
|
||||
data_manager_pinned: '[※ Snapshot marcada](#d8ff2b show_text=&7Marcada:\\n&8Essa snapshot de dados do usuário não será girada automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa do salvamento:\\n&8O motivo para que os dados fossem salvos)\\n'
|
||||
data_manager_title: '[Visualizando snapshot dos dados do usuário](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8Quando os dados foram salvos)'
|
||||
data_manager_pinned: '[※ Snapshot marcada](#d8ff2b show_text=&7Marcada:\n&8Essa snapshot de dados do usuário não será girada automaticamente.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Causa do salvamento:\n&8O motivo para que os dados fossem salvos)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Pontos de Vida) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Pontos de vida) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Progressos: %1%](color=#ffc43b-#f5c962 show_text=&7Progressos que você tem realizado em:\\n&8%2%) [⌛ Tempo de jogo: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo de jogo dentro do jogo\\n&8⚠ Com base em estatísticas dentro do jogo)'
|
||||
data_manager_item_buttons: '[[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Clique para ver run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clique para ver run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Gerenciar:](gray) [[❌ Deletar…]](#ff3300 show_text=&7Clique para deletar esta snapshot de dados do usuário\\n&8Isto não afetará os dados atuais do usuário.\\n&#ff3300&⚠ Isto não pode ser desfeito! run_command=/userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Clique para restaurar estes dados do usuário.\\n&8Isto substituirá os dados atuais do usuário para os da snapshot.\\n&#ff3300&⚠ %1%''s os dados atuais serão substituídos! run_command=/userdata restore %1% %2%) [[※ Marcar/Desmarcar…]](#d8ff2b show_text=&7Clique para marcar ou desmarcar este snapshot de dados do usuário\\n&8Snapshots marcadas não serão giradas automaticamente run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Progressos: %1%](color=#ffc43b-#f5c962 show_text=&7Progressos que você tem realizado em:\n&8%2%) [⌛ Tempo de jogo: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7Tempo de jogo dentro do jogo\n&8⚠ Com base em estatísticas dentro do jogo)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Clique para ver run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Clique para ver run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Gerenciar:](gray) [[❌ Deletar…]](#ff3300 show_text=&7Clique para deletar esta snapshot de dados do usuário\n&8Isto não afetará os dados atuais do usuário.\n&#ff3300&⚠ Isto não pode ser desfeito! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restaurar…]](#00fb9a show_text=&7Clique para restaurar estes dados do usuário.\n&8Isto substituirá os dados atuais do usuário para os da snapshot.\n&#ff3300&⚠ %1%''s os dados atuais serão substituídos! suggest_command=/husksync:userdata restore %1% %2%) [[※ Marcar/Desmarcar…]](#d8ff2b show_text=&7Clique para marcar ou desmarcar este snapshot de dados do usuário\n&8Snapshots marcadas não serão giradas automaticamente run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7e %1% mais…'
|
||||
data_list_title: '[Lista de](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s snapshots de dados do usuário:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Snapshot data %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Marcado:\\n&8Snapshots marcadas não serão giradas automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\\n&8Quando os dados foram salvos run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa do salvamento:\\n&8O motivo para que os dados fossem salvos run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Snapshot data %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Marcado:\n&8Snapshots marcadas não serão giradas automaticamente. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8Quando os dados foram salvos run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Causa do salvamento:\n&8O motivo para que os dados fossem salvos run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Snapshot de dados do usuário deletada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Restaurada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Snapshot de dados do usuário marcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Snapshot de dados do usuário desmarcada com sucesso](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -10,22 +10,32 @@ error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to di
|
||||
error_invalid_version_uuid: '[Error:](#ff3300) [Could not find any user data for that version UUID.](#ff7e5e)'
|
||||
inventory_viewer_menu_title: '&0%1%''s Inventory'
|
||||
ender_chest_viewer_menu_title: '&0%1%''s Ender Chest'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold) [''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
inventory_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s inventory as of ⌚ %2%](#00fb9a)'
|
||||
ender_chest_viewer_opened: '[Viewing snapshot of](#00fb9a) [%1%](#00fb9a bold)[''s Ender Chest as of ⌚ %2%](#00fb9a)'
|
||||
data_update_complete: '[🔔 Your data has been updated!](#00fb9a)'
|
||||
data_update_failed: '[🔔 Failed to update your data! Please contact an administrator.](#ff7e5e)'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&8What caused the data to be saved)\\n'
|
||||
data_manager_title: '[Viewing user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%](#00fb9a bold show_text=&7Player UUID:\n&8%4%)[:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\n&8When the data was saved)'
|
||||
data_manager_pinned: '[※ Snapshot pinned](#d8ff2b show_text=&7Pinned:\n&8This user data snapshot won''t be automatically rotated.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7Health points) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7Hunger points) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7XP level) [🏹 %5%](dark_aqua show_text=&7Game mode)'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\\n&8⚠ Based on in-game statistics)'
|
||||
data_manager_item_buttons: '[[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\\n&8This will not affect the user''s current data.\\n&#ff3300&⚠ This cannot be undone! run_command=/userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\\n&8This will set the user''s data to this snapshot.\\n&#ff3300&⚠ %1%''s current data will be overwritten! run_command=/userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ Advancements: %1%](color=#ffc43b-#f5c962 show_text=&7Advancements you have progress in:\n&8%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7In-game play time\n&8⚠ Based on in-game statistics)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 Inventory…]](color=#a17b5f-#f5b98c show_text=&7Click to view run_command=/inventory %1% %2%) [[⌀ Ender Chest…]](#b649c4-#d254ff show_text=&7Click to view run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ Delete…]](#ff3300 show_text=&7Click to delete this snapshot of user data.\n&8This will not affect the user''s current data.\n&#ff3300&⚠ This cannot be undone! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ Restore…]](#00fb9a show_text=&7Click to restore this user data.\n&8This will set the user''s data to this snapshot.\n&#ff3300&⚠ %1%''s current data will be overwritten! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7and %1% more…'
|
||||
data_list_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\\n&8What caused the data to be saved run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&8%4%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7Data snapshot %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7Pinned:\n&8Pinned snapshots won''t be automatically rotated. run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7Version timestamp:&7\n&8When the data was saved run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7Save cause:\n&8What caused the data to be saved run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ Successfully deleted user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_restored: '[⏪ Successfully restored](#00fb9a) [%1%](#00fb9a show_text=&7Player UUID:\n&8%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ Successfully pinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_unpinned: '[※ Successfully unpinned user data snapshot](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\n&8%4%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -14,18 +14,28 @@ inventory_viewer_opened: '[正在查看玩家](#00fb9a) [%1%](#00fb9a bold) [于
|
||||
ender_chest_viewer_opened: '[正在查看玩家](#00fb9a) [%1%](#00fb9a bold) [于 ⌚ %2% 的末影箱备份](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的用户数据已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 无法更新你的用户数据! 请联系管理员.](#ff7e5e)'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\\n&8%4%) [的数据备份](#00fb9a) [%1%](#00fb9a show_text=&7备份版本 UUID:\\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7备份时间:\\n&7何时保存了此数据)'
|
||||
data_manager_pinned: '[※ 置顶备份](#d8ff2b show_text=&7置顶:\\n&8此数据备份不会按照备份时间自动排序.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7备份原因:\\n&7为何保存了此数据)\\n'
|
||||
data_manager_title: '[正在查看玩家](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%](#00fb9a show_text=&7备份版本 UUID:\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7备份时间:\n&7何时保存了此数据)'
|
||||
data_manager_pinned: '[※ 置顶备份](#d8ff2b show_text=&7置顶:\n&8此数据备份不会按照备份时间自动排序.)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7备份原因:\n&7为何保存了此数据)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7饱食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7经验等级) [🏹 %5%](dark_aqua show_text=&7游戏模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7%2%) [⌛ 游玩时间: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ 基于游戏内的统计)'
|
||||
data_manager_item_buttons: '[[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7点击查看 run_command=/inventory %1% %2%) [[⌀ 末影箱…]](#b649c4-#d254ff show_text=&7点击查看 run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ 删除…]](#ff3300 show_text=&7点击删除此数据备份.\\n这不会影响玩家当前的数据.\\n&#ff3300&⚠ 此操作不可撤销! run_command=/userdata delete %1% %2%) [[⏪ 恢复…]](#00fb9a show_text=&7点击让玩家恢复到此数据备份.\\n这将会使玩家的数据恢复到这个备份.\\n&#ff3300&⚠ %1% 当前的用户数据会被备份数据所覆盖! run_command=/userdata restore %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7%2%) [⌛ 游玩时间: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ 基于游戏内的统计)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7点击查看 run_command=/inventory %1% %2%) [[⌀ 末影箱…]](#b649c4-#d254ff show_text=&7点击查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[Manage:](gray) [[❌ 删除…]](#ff3300 show_text=&7点击删除此数据备份.\n这不会影响玩家当前的数据.\n&#ff3300&⚠ 此操作不可撤销! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢复…]](#00fb9a show_text=&7点击让玩家恢复到此数据备份.\n这将会使玩家的数据恢复到这个备份.\n&#ff3300&⚠ %1% 当前的用户数据会被备份数据所覆盖! suggest_command=/husksync:userdata restore %1% %2%) [[※ Pin/Unpin…]](#d8ff2b show_text=&7Click to pin or unpin this user data snapshot\n&8Pinned snapshots won''t be automatically rotated run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7还有 %1% …'
|
||||
data_list_title: '[玩家](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[的数据备份列表:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7备份 %3% run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7备份时间&7\\n&8何时保存了此数据 run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7备份版本 UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7备份原因\\n&8为何保存了此数据 run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本 UUID:\\n&7%4%)'
|
||||
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\\n&8%2%)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7备份 %3% run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7备份时间&7\n&8何时保存了此数据 run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7备份版本 UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7备份原因\n&8为何保存了此数据 run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ 成功删除玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&7%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\n&7%2%)'
|
||||
data_restored: '[⏪ 成功恢复玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&7%2%)[的数据备份](#00fb9a) [%3%.](#00fb9a show_text=&7备份版本 UUID:\n&7%4%)'
|
||||
data_pinned: '[※ 成功置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功取消置顶玩家](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的数据备份](#00fb9a) [%1%.](#00fb9a show_text=&7备份版本 UUID:\n&8%2%)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -14,18 +14,28 @@ inventory_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的
|
||||
ender_chest_viewer_opened: '[查看](#00fb9a) [%1%](#00fb9a bold) [於 ⌚ %2% 的終界箱快照資料](#00fb9a)'
|
||||
data_update_complete: '[🔔 你的資料已更新!](#00fb9a)'
|
||||
data_update_failed: '[🔔 無法更新您的資料! 請聯繫管理員](#ff7e5e)'
|
||||
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\\n&8何時保存的資料)'
|
||||
data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\\n&8此快照資料不會自動輪換更新)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\\n&8保存此快照的原因)\\n'
|
||||
data_manager_title: '[查看](#00fb9a) [%3%](#00fb9a bold show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [:](#00fb9a)'
|
||||
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7快照時間:\n&8何時保存的資料)'
|
||||
data_manager_pinned: '[※ 被標記的快照](#d8ff2b show_text=&7標記:\n&8此快照資料不會自動輪換更新)'
|
||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因)\n'
|
||||
data_manager_status: '[%1%](red)[/](gray)[%2%](red)[×](gray)[❤](red show_text=&7血量) [%3%](yellow)[×](gray)[🍖](yellow show_text=&7飽食度) [ʟᴠ](green)[.](gray)[%4%](green show_text=&7經驗等級) [🏹 %5%](dark_aqua show_text=&7遊戲模式)'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\\n&8⚠ 根據遊戲內統計)'
|
||||
data_manager_item_buttons: '[[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)\\n'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\\n&8這不會影像目前玩家的資料\\n&#ff3300&⚠ 此操作不能取消! run_command=/userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\\n&8這將導致玩家的資料會被此快照覆蓋\\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! run_command=/userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\\n&8被標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)\\n'
|
||||
data_manager_advancements_statistics: '[⭐ 成就: %1%](color=#ffc43b-#f5c962 show_text=&7已獲得的成就:\n&8%2%) [⌛ 遊戲時間: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7遊戲內的遊玩時間\n&8⚠ 根據遊戲內統計)\n'
|
||||
data_manager_item_buttons: '[View:](gray) [[🪣 背包…]](color=#a17b5f-#f5b98c show_text=&7點擊查看 run_command=/inventory %1% %2%) [[⌀ 終界箱…]](#b649c4-#d254ff show_text=&7點擊查看 run_command=/enderchest %1% %2%)'
|
||||
data_manager_management_buttons: '[管理:](gray) [[❌ 刪除…]](#ff3300 show_text=&7點擊刪除這個快照\n&8這不會影像目前玩家的資料\n&#ff3300&⚠ 此操作不能取消! suggest_command=/husksync:userdata delete %1% %2%) [[⏪ 恢復…]](#00fb9a show_text=&7點擊將玩家資料覆蓋為此快照\n&8這將導致玩家的資料會被此快照覆蓋\n&#ff3300&⚠ %1% 當前的資料將被覆蓋! suggest_command=/husksync:userdata restore %1% %2%) [[※ 標記…]](#d8ff2b show_text=&7點擊切換標記狀態\n&8被標記的快照將不會自動輪換更新 run_command=/userdata pin %1% %2%)'
|
||||
data_manager_system_buttons: '[System:](gray) [[⏷ File Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to a file.\n&8Data dumps can be found in ~/plugins/HuskSync/dumps/ run_command=/husksync:userdata dump %1% %2% file) [[☂ Web Dump…]](dark_gray show_text=&7Click to dump this raw user data snapshot to the mc-logs service\n&8You will be provided with a URL containing the data. run_command=/husksync:userdata dump %1% %2% web)'
|
||||
data_manager_advancements_preview_remaining: '&7還有 %1% …'
|
||||
data_list_title: '[%1%](#00fb9a bold show_text=&7UUID: %2%) [的快照資料列表:](#00fb9a)\\n'
|
||||
data_list_item: '[%1%](gray show_text=&7快照名稱: %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7標記:\\n&8被標記的快照不會自動輪換更新 run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7時間戳:&7\\n&8資料保存時間 run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7保存原因:\\n&8保存此快照的原因 run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%)'
|
||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&8%4%)'
|
||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%)'
|
||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\\n&8%2%) [的標記](#00fb9a)'
|
||||
data_list_title: '[%1%''s user data snapshots:](#00fb9a) [(%2%-%3% of](#00fb9a) [%4%](#00fb9a bold)[)](#00fb9a)\n'
|
||||
data_list_item: '[%1%](gray show_text=&7快照名稱: %3% run_command=/userdata view %6% %4%) [%7%](#d8ff2b show_text=&7標記:\n&8被標記的快照不會自動輪換更新 run_command=/userdata view %6% %4%) [%2%](color=#ffc43b-#f5c962 show_text=&7時間戳:&7\n&8資料保存時間 run_command=/userdata view %6% %4%) [⚡ %3%](color=#62a9f5-#7ab8fa show_text=&7Version UUID:&7\n&8%4% run_command=/userdata view %6% %4%) [⚑ %5%](#23a825-#36f539 show_text=&7保存原因:\n&8保存此快照的原因 run_command=/userdata view %6% %4%)'
|
||||
data_deleted: '[❌ 成功刪除:](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_restored: '[⏪ 成功將玩家](#00fb9a) [%1%](#00fb9a show_text=&7玩家 UUID:\n&8%2%)[的資料恢復為 快照:](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\n&8%4%)'
|
||||
data_pinned: '[※ 成功標記](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [的快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%)'
|
||||
data_unpinned: '[※ 成功解除](#00fb9a) [%3%](#00fb9a show_text=&7玩家 UUID:\n&8%4%) [快照:](#00fb9a) [%1%](#00fb9a show_text=&7Version UUID:\n&8%2%) [的標記](#00fb9a)'
|
||||
data_dumped: '[☂ Successfully dumped the user data snapshot %1% for %2% to:](#00fb9a) &7%3%'
|
||||
list_footer: '\n%1%[Page](#00fb9a) [%2%](#00fb9a)/[%3%](#00fb9a)%4% %5%'
|
||||
list_previous_page_button: '[◀](white show_text=&7View previous page run_command=%2% %1%) '
|
||||
list_next_page_button: ' [▶](white show_text=&7View next page run_command=%2% %1%)'
|
||||
list_page_jumpers: '(%1%)'
|
||||
list_page_jumper_button: '[%1%](show_text=&7Jump to page %1% run_command=%2% %1%)'
|
||||
list_page_jumper_current_page: '[%1%](#00fb9a)'
|
||||
list_page_jumper_separator: ' '
|
||||
list_page_jumper_group_separator: '…'
|
||||
@@ -1,5 +1,6 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.logger.DummyLogger;
|
||||
import net.william278.husksync.player.DummyPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
@@ -7,7 +8,7 @@ import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
@@ -18,36 +19,25 @@ public class DataAdaptionTests {
|
||||
@Test
|
||||
public void testJsonDataAdapter() {
|
||||
final OnlineUser dummyUser = DummyPlayer.create();
|
||||
final AtomicBoolean isEquals = new AtomicBoolean(false);
|
||||
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> {
|
||||
dummyUser.getUserData(new DummyLogger(), new Settings()).join().ifPresent(dummyUserData -> {
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final UserData deserializedUserData = dataAdapter.fromBytes(data);
|
||||
|
||||
isEquals.set(deserializedUserData.getInventoryData().serializedItems
|
||||
.equals(dummyUserData.getInventoryData().serializedItems)
|
||||
&& deserializedUserData.getEnderChestData().serializedItems
|
||||
.equals(dummyUserData.getEnderChestData().serializedItems)
|
||||
&& deserializedUserData.getPotionEffectsData().serializedPotionEffects
|
||||
.equals(dummyUserData.getPotionEffectsData().serializedPotionEffects)
|
||||
&& deserializedUserData.getStatusData().health == dummyUserData.getStatusData().health
|
||||
&& deserializedUserData.getStatusData().hunger == dummyUserData.getStatusData().hunger
|
||||
&& deserializedUserData.getStatusData().saturation == dummyUserData.getStatusData().saturation
|
||||
&& deserializedUserData.getStatusData().saturationExhaustion == dummyUserData.getStatusData().saturationExhaustion
|
||||
&& deserializedUserData.getStatusData().selectedItemSlot == dummyUserData.getStatusData().selectedItemSlot
|
||||
&& deserializedUserData.getStatusData().totalExperience == dummyUserData.getStatusData().totalExperience
|
||||
&& deserializedUserData.getStatusData().maxHealth == dummyUserData.getStatusData().maxHealth
|
||||
&& deserializedUserData.getStatusData().healthScale == dummyUserData.getStatusData().healthScale);
|
||||
// Assert all deserialized data is equal to the original data
|
||||
Assertions.assertTrue(dummyUserData.getInventory().isPresent());
|
||||
Assertions.assertTrue(deserializedUserData.getInventory().isPresent());
|
||||
Assertions.assertEquals(dummyUserData.getInventory().get().serializedItems, deserializedUserData.getInventory().get().serializedItems);
|
||||
Assertions.assertEquals(dummyUserData.getFormatVersion(), deserializedUserData.getFormatVersion());
|
||||
});
|
||||
Assertions.assertTrue(isEquals.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJsonFormat() {
|
||||
final OnlineUser dummyUser = DummyPlayer.create();
|
||||
final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_items\":\"\"},\"ender_chest\":{\"serialized_items\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"location\":{\"world_name\":\"dummy_world\",\"world_uuid\":\"00000000-0000-0000-0000-000000000000\",\"world_environment\":\"NORMAL\",\"x\":0.0,\"y\":64.0,\"z\":0.0,\"yaw\":90.0,\"pitch\":180.0},\"persistent_data_container\":{\"persistent_data_map\":{}},\"minecraft_version\":\"1.19-beta123456\",\"format_version\":1}";
|
||||
final String expectedJson = "{\"status\":{\"health\":20.0,\"max_health\":20.0,\"health_scale\":0.0,\"hunger\":20,\"saturation\":5.0,\"saturation_exhaustion\":5.0,\"selected_item_slot\":1,\"total_experience\":100,\"experience_level\":1,\"experience_progress\":1.0,\"game_mode\":\"SURVIVAL\",\"is_flying\":false},\"inventory\":{\"serialized_items\":\"\"},\"ender_chest\":{\"serialized_items\":\"\"},\"potion_effects\":{\"serialized_potion_effects\":\"\"},\"advancements\":[],\"statistics\":{\"untyped_statistics\":{},\"block_statistics\":{},\"item_statistics\":{},\"entity_statistics\":{}},\"minecraft_version\":\"1.19\",\"format_version\":3}";
|
||||
AtomicReference<String> json = new AtomicReference<>();
|
||||
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> {
|
||||
dummyUser.getUserData(new DummyLogger(), new Settings()).join().ifPresent(dummyUserData -> {
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
json.set(new String(data, StandardCharsets.UTF_8));
|
||||
@@ -58,28 +48,40 @@ public class DataAdaptionTests {
|
||||
@Test
|
||||
public void testCompressedDataAdapter() {
|
||||
final OnlineUser dummyUser = DummyPlayer.create();
|
||||
AtomicBoolean isEquals = new AtomicBoolean(false);
|
||||
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> {
|
||||
dummyUser.getUserData(new DummyLogger(), new Settings()).join().ifPresent(dummyUserData -> {
|
||||
final DataAdapter dataAdapter = new CompressedDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final UserData deserializedUserData = dataAdapter.fromBytes(data);
|
||||
|
||||
isEquals.set(deserializedUserData.getInventoryData().serializedItems
|
||||
.equals(dummyUserData.getInventoryData().serializedItems)
|
||||
&& deserializedUserData.getEnderChestData().serializedItems
|
||||
.equals(dummyUserData.getEnderChestData().serializedItems)
|
||||
&& deserializedUserData.getPotionEffectsData().serializedPotionEffects
|
||||
.equals(dummyUserData.getPotionEffectsData().serializedPotionEffects)
|
||||
&& deserializedUserData.getStatusData().health == dummyUserData.getStatusData().health
|
||||
&& deserializedUserData.getStatusData().hunger == dummyUserData.getStatusData().hunger
|
||||
&& deserializedUserData.getStatusData().saturation == dummyUserData.getStatusData().saturation
|
||||
&& deserializedUserData.getStatusData().saturationExhaustion == dummyUserData.getStatusData().saturationExhaustion
|
||||
&& deserializedUserData.getStatusData().selectedItemSlot == dummyUserData.getStatusData().selectedItemSlot
|
||||
&& deserializedUserData.getStatusData().totalExperience == dummyUserData.getStatusData().totalExperience
|
||||
&& deserializedUserData.getStatusData().maxHealth == dummyUserData.getStatusData().maxHealth
|
||||
&& deserializedUserData.getStatusData().healthScale == dummyUserData.getStatusData().healthScale);
|
||||
// Assert all deserialized data is equal to the original data
|
||||
Assertions.assertTrue(dummyUserData.getInventory().isPresent());
|
||||
Assertions.assertTrue(deserializedUserData.getInventory().isPresent());
|
||||
Assertions.assertEquals(dummyUserData.getInventory().get().serializedItems, deserializedUserData.getInventory().get().serializedItems);
|
||||
Assertions.assertEquals(dummyUserData.getFormatVersion(), deserializedUserData.getFormatVersion());
|
||||
});
|
||||
Assertions.assertTrue(isEquals.get());
|
||||
}
|
||||
|
||||
private String getTestSerializedPersistentDataContainer() {
|
||||
final HashMap<String, PersistentDataTag<?>> persistentDataTest = new HashMap<>();
|
||||
persistentDataTest.put("husksync:byte_test", new PersistentDataTag<>(BukkitPersistentDataTagType.BYTE, 0x01));
|
||||
persistentDataTest.put("husksync:double_test", new PersistentDataTag<>(BukkitPersistentDataTagType.DOUBLE, 2d));
|
||||
persistentDataTest.put("husksync:string_test", new PersistentDataTag<>(BukkitPersistentDataTagType.STRING, "test"));
|
||||
persistentDataTest.put("husksync:int_test", new PersistentDataTag<>(BukkitPersistentDataTagType.INTEGER, 3));
|
||||
persistentDataTest.put("husksync:long_test", new PersistentDataTag<>(BukkitPersistentDataTagType.LONG, 4L));
|
||||
persistentDataTest.put("husksync:float_test", new PersistentDataTag<>(BukkitPersistentDataTagType.FLOAT, 5f));
|
||||
persistentDataTest.put("husksync:short_test", new PersistentDataTag<>(BukkitPersistentDataTagType.SHORT, 6));
|
||||
final PersistentDataContainerData persistentDataContainerData = new PersistentDataContainerData(persistentDataTest);
|
||||
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
UserData userData = new UserData();
|
||||
userData.persistentDataContainerData = persistentDataContainerData;
|
||||
return dataAdapter.toJson(userData, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPersistentDataContainerSerialization() {
|
||||
Assertions.assertEquals(getTestSerializedPersistentDataContainer(), "{\"persistent_data_container\":{\"persistent_data_map\":{\"husksync:int_test\":{\"type\":\"INTEGER\",\"value\":3},\"husksync:string_test\":{\"type\":\"STRING\",\"value\":\"test\"},\"husksync:long_test\":{\"type\":\"LONG\",\"value\":4},\"husksync:byte_test\":{\"type\":\"BYTE\",\"value\":1},\"husksync:short_test\":{\"type\":\"SHORT\",\"value\":6},\"husksync:double_test\":{\"type\":\"DOUBLE\",\"value\":2.0},\"husksync:float_test\":{\"type\":\"FLOAT\",\"value\":5.0}}},\"format_version\":3}");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package net.william278.husksync.logger;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@@ -22,11 +21,6 @@ public class DummyLogger extends Logger {
|
||||
System.out.println(level.getName() + ": " + message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull MineDown mineDown) {
|
||||
System.out.println(level.getName() + ": " + mineDown.message());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(@NotNull String message) {
|
||||
System.out.println(Level.INFO.getName() + ": " + message);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package net.william278.husksync.player;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
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.husksync.util.Version;
|
||||
import net.william278.desertwell.Version;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class DummyPlayer extends OnlineUser {
|
||||
@@ -31,7 +28,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
|
||||
});
|
||||
@@ -132,7 +129,7 @@ public class DummyPlayer extends OnlineUser {
|
||||
@NotNull
|
||||
@Override
|
||||
public Version getMinecraftVersion() {
|
||||
return Version.minecraftVersion("1.19-beta123456");
|
||||
return Version.fromMinecraftVersionString("1.19-beta123456");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,8 +148,16 @@ public class DummyPlayer extends OnlineUser {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showMenu(@NotNull ItemEditorMenu menu) {
|
||||
public CompletableFuture<Optional<ItemData>> showMenu(@NotNull ItemData itemData, boolean editable,
|
||||
int minimumRows, @NotNull MineDown title) {
|
||||
// do nothing
|
||||
return CompletableFuture.completedFuture(Optional.empty());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,5 +3,10 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
javaVersion=16
|
||||
|
||||
plugin_version=2.0.1
|
||||
plugin_version=2.1
|
||||
plugin_archive=husksync
|
||||
|
||||
jedis_version=4.2.3
|
||||
mysql_driver_version=8.0.30
|
||||
snappy_version=1.1.8.4
|
||||
commons_text_version=1.9
|
||||
BIN
images/data-dumping.png
Normal file
BIN
images/data-dumping.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 111 KiB |
BIN
images/rich-syntax-highlighting.png
Normal file
BIN
images/rich-syntax-highlighting.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 430 KiB |
@@ -1,6 +1,5 @@
|
||||
dependencies {
|
||||
implementation project(path: ':bukkit', configuration: 'shadow')
|
||||
implementation project(path: ':api', configuration: 'shadow')
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
|
||||
@@ -8,5 +8,4 @@ rootProject.name = 'HuskSync'
|
||||
|
||||
include 'common'
|
||||
include 'bukkit'
|
||||
include 'api'
|
||||
include 'plugin'
|
||||
Reference in New Issue
Block a user