9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-21 07:49:13 +00:00

Compare commits

..

24 Commits
2.0.1 ... 2.0.2

Author SHA1 Message Date
William
390a77b407 Update README.md (remove mariadb comment) 2022-08-21 13:30:37 +01:00
William
04ab9d14f8 Update rich-syntax-highlighting.png 2022-08-21 13:22:23 +01:00
William
3a32d481c4 Add rich-syntax-highlighting.png 2022-08-21 13:20:04 +01:00
William
8edbc029f8 Remove test commodore completions 2022-08-21 13:15:48 +01:00
William
258356e45d Bundle boosted-yaml, adjust shading and build scripts 2022-08-21 13:09:30 +01:00
William
e1628b6448 Download jedis at runtime 2022-08-21 12:51:58 +01:00
William
fa32e97564 Bump mysql-connector-java to 8.0.30 2022-08-21 12:33:47 +01:00
William
8080d57645 Update MPDB migrator messages 2022-08-21 12:22:33 +01:00
William
0a2f7b6cd4 Fix migration reporting for MPDB migrator too 2022-08-21 12:21:56 +01:00
William
26a2366876 Fix inaccurate migration progress tracking 2022-08-21 12:20:19 +01:00
William
2690ab3144 Make initialization exception more obvious, close #47 2022-08-21 11:54:51 +01:00
William
18b96944e9 Tweak migrator to ensure safe user setting on large data imports 2022-08-21 11:49:38 +01:00
William
3282f5739c Fix nested futures on #ensureUser causing ineffective #join synchronisation calls, close #40 2022-08-21 11:49:15 +01:00
William
50e66be0c0 Relocate DummySettings 2022-08-08 19:49:10 +01:00
William
593c88c8ba Fix tests, create DummySettings 2022-08-08 19:44:12 +01:00
William
2f700b2d93 Use Commodore for rich command completion registering 2022-08-08 19:32:09 +01:00
William
d1c95030f0 Added config option for syncing dead player inventories, cancel damage if locked 2022-08-04 17:48:16 +01:00
William
1ed2414241 Add Bulgarian (bg-bg) courtesy of Pukejoy_1 2022-08-04 13:31:28 +01:00
William
8847483ff8 Correct Persistent Data serialization 2022-07-21 14:23:27 +01:00
William
31552f85e4 Fix malformed events 2022-07-19 14:18:41 +01:00
William
125f142cf5 Fix tests 2022-07-19 11:28:26 +01:00
William
dc3882e47e Merge remote-tracking branch 'origin/master' 2022-07-19 11:27:00 +01:00
William
dafbcad10e Fix PersistentDataContainer synchronisation, bump to v2.0.2 2022-07-19 11:26:56 +01:00
William
d1085ca7bd Update README.md 2022-07-16 17:51:53 +01:00
44 changed files with 583 additions and 108 deletions

View File

@@ -15,7 +15,7 @@
- Supports segregating synchronisation across multiple distinct clusters on one network. - Supports segregating synchronisation across multiple distinct clusters on one network.
## Requirements ## Requirements
* A MySQL Database (v8.0+) * A MySQL Database (v8.0+).
* A Redis Database (v5.0+) * A Redis Database (v5.0+)
* Any number of proxied Spigot servers (Minecraft v1.16.5+) * Any number of proxied Spigot servers (Minecraft v1.16.5+)

View File

@@ -7,6 +7,10 @@ dependencies {
} }
shadowJar { shadowJar {
dependencies {
exclude(dependency('com.mojang:brigadier'))
}
relocate 'org.apache', 'net.william278.husksync.libraries' relocate 'org.apache', 'net.william278.husksync.libraries'
relocate 'dev.dejvokep', 'net.william278.husksync.libraries' relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries' relocate 'de.themoep', 'net.william278.husksync.libraries'
@@ -16,6 +20,7 @@ shadowJar {
relocate 'com.google', 'net.william278.husksync.libraries' relocate 'com.google', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries' relocate 'redis.clients', 'net.william278.husksync.libraries'
relocate 'org.json', 'net.william278.husksync.libraries.json' relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'

View File

@@ -10,6 +10,9 @@ version "$ext.plugin_version+${versionMetadata()}"
ext { ext {
set 'version', version.toString() set 'version', version.toString()
set 'jedis_version', jedis_version.toString()
set 'mysql_driver_version', mysql_driver_version.toString()
set 'snappy_version', snappy_version.toString()
} }
import org.apache.tools.ant.filters.ReplaceTokens import org.apache.tools.ant.filters.ReplaceTokens
@@ -31,11 +34,12 @@ allprojects {
maven { url 'https://repo.minebench.de/' } maven { url 'https://repo.minebench.de/' }
maven { url 'https://repo.alessiodp.com/releases/' } maven { url 'https://repo.alessiodp.com/releases/' }
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url 'https://libraries.minecraft.net/' }
} }
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.8.2' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
} }
test { test {

View File

@@ -3,25 +3,31 @@ dependencies {
implementation 'org.bstats:bstats-bukkit:3.0.0' implementation 'org.bstats:bstats-bukkit:3.0.0'
implementation 'net.william278:mpdbdataconverter:1.0.1' implementation 'net.william278:mpdbdataconverter:1.0.1'
implementation 'net.william278:hsldataconverter:1.0' implementation 'net.william278:hsldataconverter:1.0'
implementation 'me.lucko:commodore:2.2'
compileOnly 'redis.clients:jedis:4.2.3'
compileOnly 'commons-io:commons-io:2.11.0' compileOnly 'commons-io:commons-io:2.11.0'
compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT' compileOnly 'de.themoep:minedown:1.7.1-SNAPSHOT'
compileOnly 'dev.dejvokep:boosted-yaml:1.2'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'dev.dejvokep:boosted-yaml:1.3'
compileOnly 'com.zaxxer:HikariCP:5.0.1' compileOnly 'com.zaxxer:HikariCP:5.0.1'
testImplementation 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
} }
shadowJar { 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 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'com.google', 'net.william278.husksync.libraries' relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'me.lucko.commodore', 'net.william278.husksync.libraries.commodore'
relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby' relocate 'net.byteflux.libby', 'net.william278.husksync.libraries.libby'
relocate 'org.bstats', 'net.william278.husksync.libraries.bstats' relocate 'org.bstats', 'net.william278.husksync.libraries.bstats'
relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter' relocate 'net.william278.mpdbconverter', 'net.william278.husksync.libraries.mpdbconverter'

View File

@@ -184,7 +184,19 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
CompletableFuture.runAsync(() -> new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole()); CompletableFuture.runAsync(() -> new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole());
} }
} catch (HuskSyncInitializationException exception) { } 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); initialized.set(false);
} catch (Exception exception) { } catch (Exception exception) {
getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception); getLoggingAdapter().log(Level.SEVERE, "An unhandled exception occurred initializing HuskSync!", exception);
@@ -268,6 +280,12 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
return logger; return logger;
} }
@NotNull
@Override
public ResourceReader getResourceReader() {
return resourceReader;
}
@Override @Override
public @NotNull Version getPluginVersion() { public @NotNull Version getPluginVersion() {
return Version.pluginVersion(getDescription().getVersion()); return Version.pluginVersion(getDescription().getVersion());

View File

@@ -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);
}
}
}

View File

@@ -1,6 +1,7 @@
package net.william278.husksync.command; 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 net.william278.husksync.player.BukkitPlayer;
import org.bukkit.command.*; import org.bukkit.command.*;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@@ -18,14 +19,14 @@ public class BukkitCommand implements CommandExecutor, TabExecutor {
/** /**
* The {@link CommandBase} that will be executed * The {@link CommandBase} that will be executed
*/ */
private final CommandBase command; protected final CommandBase command;
/** /**
* The implementing plugin * 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.command = command;
this.plugin = implementor; this.plugin = implementor;
} }
@@ -40,6 +41,9 @@ public class BukkitCommand implements CommandExecutor, TabExecutor {
pluginCommand.setTabCompleter(this); pluginCommand.setTabCompleter(this);
pluginCommand.setPermission(command.permission); pluginCommand.setPermission(command.permission);
pluginCommand.setDescription(command.getDescription()); pluginCommand.setDescription(command.getDescription());
if (CommodoreProvider.isSupported()) {
BrigadierUtil.registerCommodore(plugin, pluginCommand, command);
}
} }
@Override @Override

View File

@@ -7,6 +7,7 @@ import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable { public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList(); private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled = false; private boolean cancelled = false;
@@ -57,4 +58,8 @@ public class BukkitDataSaveEvent extends BukkitEvent implements DataSaveEvent, C
public HandlerList getHandlers() { public HandlerList getHandlers() {
return HANDLER_LIST; return HANDLER_LIST;
} }
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
} }

View File

@@ -3,11 +3,16 @@ package net.william278.husksync.event;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.event.Event; import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@SuppressWarnings("unused")
public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event { public abstract class BukkitEvent extends Event implements net.william278.husksync.event.Event {
private static final HandlerList HANDLER_LIST = new HandlerList();
protected BukkitEvent() { protected BukkitEvent() {
} }
@@ -26,4 +31,14 @@ public abstract class BukkitEvent extends Event implements net.william278.husksy
return eventFireFuture; return eventFireFuture;
} }
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
} }

View File

@@ -5,12 +5,16 @@ import net.william278.husksync.player.BukkitPlayer;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@SuppressWarnings("unused")
public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent { public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
protected final Player player; protected final Player player;
protected BukkitPlayerEvent(@NotNull Player player) { protected BukkitPlayerEvent(@NotNull Player player) {
@@ -32,4 +36,15 @@ public abstract class BukkitPlayerEvent extends BukkitEvent implements PlayerEve
return eventFireFuture; return eventFireFuture;
} }
@NotNull
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
} }

View File

@@ -6,6 +6,7 @@ import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable { public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEvent, Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList(); private static final HandlerList HANDLER_LIST = new HandlerList();
private boolean cancelled = false; private boolean cancelled = false;
@@ -41,4 +42,8 @@ public class BukkitPreSyncEvent extends BukkitPlayerEvent implements PreSyncEven
public HandlerList getHandlers() { public HandlerList getHandlers() {
return HANDLER_LIST; return HANDLER_LIST;
} }
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
} }

View File

@@ -4,6 +4,7 @@ import org.bukkit.entity.Player;
import org.bukkit.event.HandlerList; import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@SuppressWarnings("unused")
public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent { public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCompleteEvent {
private static final HandlerList HANDLER_LIST = new HandlerList(); private static final HandlerList HANDLER_LIST = new HandlerList();
@@ -16,4 +17,8 @@ public class BukkitSyncCompleteEvent extends BukkitPlayerEvent implements SyncCo
public HandlerList getHandlers() { public HandlerList getHandlers() {
return HANDLER_LIST; return HANDLER_LIST;
} }
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
} }

View File

@@ -14,6 +14,7 @@ import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.block.BlockBreakEvent; import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent; import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityPickupItemEvent; import org.bukkit.event.entity.EntityPickupItemEvent;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryClickEvent;
@@ -121,6 +122,13 @@ public class BukkitEventListener extends EventListener implements Listener {
} }
} }
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true)
public void onPlayerTakeDamage(@NotNull EntityDamageEvent event) {
if (event.getEntity() instanceof Player player) {
event.setCancelled(cancelPlayerEvent(BukkitPlayer.adapt(player)));
}
}
@EventHandler(ignoreCancelled = true) @EventHandler(ignoreCancelled = true)
public void onPlayerDeath(PlayerDeathEvent event) { public void onPlayerDeath(PlayerDeathEvent event) {
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) { if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) {

View File

@@ -18,6 +18,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -68,14 +69,15 @@ public class LegacyMigrator extends Migrator {
connectionPool.setPassword(sourcePassword); connectionPool.setPassword(sourcePassword);
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase()); 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<>(); final List<LegacyData> dataToMigrate = new ArrayList<>();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" 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` 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%` FROM `%source_players_table%`
INNER JOIN `%source_data_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_players_table%"), sourcePlayersTable)
.replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) { .replaceAll(Pattern.quote("%source_data_table%"), sourceDataTable))) {
try (final ResultSet resultSet = statement.executeQuery()) { try (final ResultSet resultSet = statement.executeQuery()) {
@@ -104,7 +106,7 @@ public class LegacyMigrator extends Migrator {
resultSet.getString("location") resultSet.getString("location")
)); ));
playersMigrated++; playersMigrated++;
if (playersMigrated % 25 == 0) { if (playersMigrated % 50 == 0) {
plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players..."); plugin.getLoggingAdapter().log(Level.INFO, "Downloaded legacy data for " + playersMigrated + " players...");
} }
} }
@@ -112,14 +114,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, "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..."); plugin.getLoggingAdapter().log(Level.INFO, "Converting HuskSync 1.x data to the new user data format (this might take a while)...");
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData ->
final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> data.toUserData(hslConverter, minecraftVersion).thenAccept(convertedData -> {
plugin.getDatabase().ensureUser(data.user()).thenRun(() -> plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION) plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.LEGACY_MIGRATION)
.exceptionally(exception -> { .exceptionally(exception -> {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage()); plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate legacy data for " + data.user().username + ": " + exception.getMessage());
return null; 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!"); plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true; return true;
} catch (Exception e) { } catch (Exception e) {

View File

@@ -18,6 +18,7 @@ import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@@ -73,7 +74,7 @@ public class MpdbMigrator extends Migrator {
connectionPool.setPassword(sourcePassword); connectionPool.setPassword(sourcePassword);
connectionPool.setPoolName((getIdentifier() + "_migrator_pool").toUpperCase()); 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<>(); final List<MpdbData> dataToMigrate = new ArrayList<>();
try (final Connection connection = connectionPool.getConnection()) { try (final Connection connection = connectionPool.getConnection()) {
try (final PreparedStatement statement = connection.prepareStatement(""" try (final PreparedStatement statement = connection.prepareStatement("""
@@ -108,14 +109,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, "Completed download of " + dataToMigrate.size() + " entries from the MySQLPlayerDataBridge database!");
plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data..."); plugin.getLoggingAdapter().log(Level.INFO, "Converting raw MySQLPlayerDataBridge data to HuskSync user data (this might take a while)...");
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData ->
final AtomicInteger playersConverted = new AtomicInteger();
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter, minecraftVersion).thenAccept(convertedData -> {
plugin.getDatabase().ensureUser(data.user()).thenRun(() -> plugin.getDatabase().ensureUser(data.user()).thenRun(() ->
plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION)) plugin.getDatabase().setUserData(data.user(), convertedData, DataSaveCause.MPDB_MIGRATION))
.exceptionally(exception -> { .exceptionally(exception -> {
plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage()); plugin.getLoggingAdapter().log(Level.SEVERE, "Failed to migrate MySQLPlayerDataBridge data for " + data.user().username + ": " + exception.getMessage());
return null; 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!"); plugin.getLoggingAdapter().log(Level.INFO, "Migration complete for " + dataToMigrate.size() + " users in " + ((System.currentTimeMillis() - startTime) / 1000) + " seconds!");
return true; return true;
} catch (Exception e) { } catch (Exception e) {

View File

@@ -7,7 +7,6 @@ import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.*; import net.william278.husksync.data.*;
import net.william278.husksync.editor.ItemEditorMenu; import net.william278.husksync.editor.ItemEditorMenu;
import net.william278.husksync.util.Version; import net.william278.husksync.util.Version;
import org.apache.commons.lang.ArrayUtils;
import org.bukkit.*; import org.bukkit.*;
import org.bukkit.advancement.Advancement; import org.bukkit.advancement.Advancement;
import org.bukkit.advancement.AdvancementProgress; import org.bukkit.advancement.AdvancementProgress;
@@ -33,6 +32,20 @@ import java.util.logging.Level;
*/ */
public class BukkitPlayer extends OnlineUser { 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 Player player;
private BukkitPlayer(@NotNull Player player) { private BukkitPlayer(@NotNull Player player) {
@@ -84,7 +97,7 @@ public class BukkitPlayer extends OnlineUser {
final double currentHealth = player.getHealth(); final double currentHealth = player.getHealth();
if (statusData.health != currentHealth) { if (statusData.health != currentHealth) {
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health; final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
if (healthToSet <= 0) { if (healthToSet < 1) {
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet)); Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet));
} else { } else {
player.setHealth(healthToSet); player.setHealth(healthToSet);
@@ -402,17 +415,61 @@ public class BukkitPlayer extends OnlineUser {
if (container.isEmpty()) { if (container.isEmpty()) {
return new PersistentDataContainerData(new HashMap<>()); return new PersistentDataContainerData(new HashMap<>());
} }
final HashMap<String, Byte[]> persistentDataMap = new HashMap<>(); final HashMap<String, PersistentDataTag<?>> persistentDataMap = new HashMap<>();
// Set persistent data keys; ignore keys that we cannot synchronise as byte arrays
for (final NamespacedKey key : container.getKeys()) { for (final NamespacedKey key : container.getKeys()) {
try { PersistentDataType<?, ?> type = null;
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY))); for (PersistentDataType<?, ?> dataType : PRIMITIVE_PERSISTENT_DATA_TYPES) {
} catch (IllegalArgumentException | NullPointerException ignored) { 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); return new PersistentDataContainerData(persistentDataMap);
}).exceptionally(throwable -> { }).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(); throwable.printStackTrace();
return new PersistentDataContainerData(new HashMap<>()); return new PersistentDataContainerData(new HashMap<>());
}); });
@@ -423,14 +480,64 @@ public class BukkitPlayer extends OnlineUser {
return CompletableFuture.runAsync(() -> { return CompletableFuture.runAsync(() -> {
player.getPersistentDataContainer().getKeys().forEach(namespacedKey -> player.getPersistentDataContainer().getKeys().forEach(namespacedKey ->
player.getPersistentDataContainer().remove(namespacedKey)); player.getPersistentDataContainer().remove(namespacedKey));
persistentDataContainerData.persistentDataMap.keySet().forEach(keyString -> { persistentDataContainerData.getTags().forEach(keyString -> {
final NamespacedKey key = NamespacedKey.fromString(keyString); final NamespacedKey key = NamespacedKey.fromString(keyString);
if (key != null) { if (key != null) {
final byte[] data = ArrayUtils.toPrimitive(persistentDataContainerData // Set a tag with the given key and value. This is crying out for a refactor.
.persistentDataMap.get(keyString)); persistentDataContainerData.getTagType(keyString).ifPresentOrElse(dataType -> {
player.getPersistentDataContainer().set(key, PersistentDataType.BYTE_ARRAY, data); 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;
}); });
} }
@@ -465,6 +572,11 @@ public class BukkitPlayer extends OnlineUser {
}); });
} }
@Override
public boolean isDead() {
return player.getHealth() < 1;
}
@Override @Override
public void sendActionBar(@NotNull MineDown mineDown) { public void sendActionBar(@NotNull MineDown mineDown) {
player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent()); player.spigot().sendMessage(ChatMessageType.ACTION_BAR, mineDown.replace().toComponent());

View File

@@ -3,7 +3,6 @@ package net.william278.husksync.util;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.util.Objects; import java.util.Objects;
@@ -20,9 +19,4 @@ public class BukkitResourceReader implements ResourceReader {
return Objects.requireNonNull(plugin.getResource(fileName)); return Objects.requireNonNull(plugin.getResource(fileName));
} }
@Override
public @NotNull File getDataFolder() {
return plugin.getDataFolder();
}
} }

View File

@@ -0,0 +1,3 @@
inventory {
name brigadier:string single_word;
}

View File

@@ -0,0 +1,5 @@
husksync {
update;
about;
reload;
}

View File

@@ -0,0 +1,3 @@
enderchest {
name brigadier:string single_word;
}

View File

@@ -0,0 +1,25 @@
userdata {
view {
name brigadier:string single_word {
version brigadier:string single_word;
}
}
list {
name brigadier:string single_word;
}
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;
}
}
}

View File

@@ -9,9 +9,10 @@ softdepend:
- MysqlPlayerDataBridge - MysqlPlayerDataBridge
- Plan - Plan
libraries: libraries:
- 'mysql:mysql-connector-java:8.0.29' - 'redis.clients:jedis:${jedis_version}'
- 'org.xerial.snappy:snappy-java:1.1.8.4' - 'mysql:mysql-connector-java:${mysql_driver_version}'
- 'dev.dejvokep:boosted-yaml:1.2' - 'org.xerial.snappy:snappy-java:${snappy_version}'
commands: commands:
husksync: husksync:
usage: '/husksync <update/info/reload/migrate>' usage: '/husksync <update/info/reload/migrate>'

View File

@@ -2,30 +2,28 @@ dependencies {
implementation 'commons-io:commons-io:2.11.0' implementation 'commons-io:commons-io:2.11.0'
implementation 'de.themoep:minedown:1.7.1-SNAPSHOT' implementation 'de.themoep:minedown:1.7.1-SNAPSHOT'
implementation 'com.google.code.gson:gson:2.9.0' implementation 'com.google.code.gson:gson:2.9.0'
implementation('redis.clients:jedis:4.2.3') { implementation 'dev.dejvokep:boosted-yaml:1.3'
exclude module: 'slf4j-api'
}
implementation ('com.zaxxer:HikariCP:5.0.1') { implementation ('com.zaxxer:HikariCP:5.0.1') {
exclude module: 'slf4j-api' exclude module: 'slf4j-api'
} }
compileOnly 'dev.dejvokep:boosted-yaml:1.2' compileOnly 'redis.clients:jedis:' + jedis_version
compileOnly 'org.xerial.snappy:snappy-java:1.1.8.4' compileOnly 'org.xerial.snappy:snappy-java:' + snappy_version
compileOnly 'org.jetbrains:annotations:23.0.0' compileOnly 'org.jetbrains:annotations:23.0.0'
compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690' compileOnly 'com.github.plan-player-analytics:Plan:5.4.1690'
testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4' testImplementation 'org.xerial.snappy:snappy-java:1.1.8.4'
testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690' testImplementation 'com.github.plan-player-analytics:Plan:5.4.1690'
testCompileOnly 'dev.dejvokep:boosted-yaml:1.3'
testCompileOnly 'org.jetbrains:annotations:23.0.0' testCompileOnly 'org.jetbrains:annotations:23.0.0'
} }
shadowJar { 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 'de.themoep', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries' relocate 'org.jetbrains', 'net.william278.husksync.libraries'
relocate 'org.intellij', 'net.william278.husksync.libraries' relocate 'org.intellij', 'net.william278.husksync.libraries'
relocate 'com.zaxxer', 'net.william278.husksync.libraries' relocate 'com.zaxxer', 'net.william278.husksync.libraries'
relocate 'com.google', 'net.william278.husksync.libraries' relocate 'dev.dejvokep', 'net.william278.husksync.libraries'
relocate 'redis.clients', 'net.william278.husksync.libraries'
relocate 'org.json', 'net.william278.husksync.libraries.json'
} }

View File

@@ -10,6 +10,7 @@ import net.william278.husksync.migrator.Migrator;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import net.william278.husksync.redis.RedisManager; import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.util.Logger; import net.william278.husksync.util.Logger;
import net.william278.husksync.util.ResourceReader;
import net.william278.husksync.util.Version; import net.william278.husksync.util.Version;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -114,6 +115,14 @@ public interface HuskSync {
@NotNull @NotNull
Logger getLoggingAdapter(); Logger getLoggingAdapter();
/**
* Returns the plugin resource file reader
*
* @return the {@link ResourceReader}
*/
@NotNull
ResourceReader getResourceReader();
/** /**
* Returns the plugin version * Returns the plugin version
* *

View File

@@ -72,7 +72,7 @@ public abstract class BaseHuskSyncAPI {
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) { public final CompletableFuture<Optional<UserData>> getUserData(@NotNull User user) {
return CompletableFuture.supplyAsync(() -> { return CompletableFuture.supplyAsync(() -> {
if (user instanceof OnlineUser) { if (user instanceof OnlineUser) {
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter()).join(); return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).join();
} else { } else {
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData); return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
} }
@@ -103,7 +103,8 @@ public abstract class BaseHuskSyncAPI {
* @since 2.0 * @since 2.0
*/ */
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) { public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> optionalUserData.ifPresent( return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings())
.thenAccept(optionalUserData -> optionalUserData.ifPresent(
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join()))); userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
} }

View File

@@ -9,6 +9,7 @@ import net.william278.husksync.util.UpdateChecker;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.logging.Level; import java.util.logging.Level;
@@ -126,10 +127,13 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
@Override @Override
public List<String> onTabComplete(@NotNull String[] args) { public List<String> onTabComplete(@NotNull String[] args) {
if (args.length <= 1) {
return Arrays.stream(COMMAND_ARGUMENTS) 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()); .sorted().collect(Collectors.toList());
} }
return Collections.emptyList();
}
private void displayPluginInformation(@NotNull OnlineUser player) { private void displayPluginInformation(@NotNull OnlineUser player) {
if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) { if (!player.hasPermission(Permission.COMMAND_HUSKSYNC_INFO.node)) {

View File

@@ -225,7 +225,7 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
switch (args.length) { switch (args.length) {
case 0, 1 -> { case 0, 1 -> {
return Arrays.stream(COMMAND_ARGUMENTS) 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()); .sorted().collect(Collectors.toList());
} }
case 2 -> { case 2 -> {

View File

@@ -19,7 +19,7 @@ public class Locales {
[A modern, cross-server player data synchronization system](gray) [A modern, cross-server player data synchronization system](gray)
[• Author:](white) [William278](gray show_text=&7Click to visit website open_url=https://william278.net) [• 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) [• 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) [• 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), [Pukejoy_1](gray show_text=&7Bulgarian, bg-bg), [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/) [• 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) [• 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)"""; [• Discord support:](white) [[Link]](#00fb9a show_text=&7Click to join open_url=https://discord.gg/tVYhJfyDWG)""";

View File

@@ -4,10 +4,7 @@ import dev.dejvokep.boostedyaml.YamlDocument;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
/** /**
* Settings used for the plugin, as read from the config file * Settings used for the plugin, as read from the config file
@@ -17,7 +14,7 @@ public class Settings {
/** /**
* Map of {@link ConfigOption}s read from the config file * Map of {@link ConfigOption}s read from the config file
*/ */
private final HashMap<ConfigOption, Object> configOptions; private final Map<ConfigOption, Object> configOptions;
// Load the settings from the document // Load the settings from the document
private Settings(@NotNull YamlDocument config) { private Settings(@NotNull YamlDocument config) {
@@ -33,6 +30,11 @@ public class Settings {
})); }));
} }
// Default constructor for empty settings
protected Settings(@NotNull Map<ConfigOption, Object> configOptions) {
this.configOptions = configOptions;
}
/** /**
* Get the value of the specified {@link ConfigOption} * Get the value of the specified {@link ConfigOption}
* *
@@ -144,6 +146,7 @@ public class Settings {
SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true), SYNCHRONIZATION_SAVE_ON_WORLD_SAVE("synchronization.save_on_world_save", OptionType.BOOLEAN, true),
SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true), SYNCHRONIZATION_COMPRESS_DATA("synchronization.compress_data", OptionType.BOOLEAN, true),
SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500), SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS("synchronization.network_latency_milliseconds", OptionType.INTEGER, 500),
SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES("synchronization.save_dead_player_inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true), SYNCHRONIZATION_SYNC_INVENTORIES("synchronization.features.inventories", OptionType.BOOLEAN, true),
SYNCHRONIZATION_SYNC_ENDER_CHESTS("synchronization.features.ender_chests", 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_HEALTH("synchronization.features.health", OptionType.BOOLEAN, true),

View File

@@ -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();
}
}

View File

@@ -4,6 +4,8 @@ import com.google.gson.annotations.SerializedName;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Map; 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 * 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 * Map of namespaced key strings to a byte array representing the persistent data
*/ */
@SerializedName("persistent_data_map") @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; this.persistentDataMap = persistentDataMap;
} }
@@ -24,4 +26,23 @@ public class PersistentDataContainerData {
protected 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();
}
} }

View File

@@ -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);
}
}

View File

@@ -15,7 +15,7 @@ public class UserData {
* </p> * </p>
* This value is to be incremented whenever the format changes. * 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 = 2;
/** /**
* Stores the user's status data, including health, food, etc. * Stores the user's status data, including health, food, etc.

View File

@@ -14,6 +14,7 @@ import org.jetbrains.annotations.NotNull;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
@@ -108,7 +109,7 @@ public abstract class Database {
*/ */
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
protected final String[] getSchemaStatements(@NotNull String schemaFileName) throws IOException { 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(";"); .readAllBytes(), StandardCharsets.UTF_8)).split(";");
} }

View File

@@ -121,7 +121,7 @@ public class MySqlDatabase extends Database {
@Override @Override
public CompletableFuture<Void> ensureUser(@NotNull User user) { public CompletableFuture<Void> ensureUser(@NotNull User user) {
return CompletableFuture.runAsync(() -> getUser(user.uuid).thenAccept(optionalUser -> return getUser(user.uuid).thenAccept(optionalUser ->
optionalUser.ifPresentOrElse(existingUser -> { optionalUser.ifPresentOrElse(existingUser -> {
if (!existingUser.username.equals(user.username)) { if (!existingUser.username.equals(user.username)) {
// Update a user's name if it has changed in the database // Update a user's name if it has changed in the database
@@ -155,7 +155,7 @@ public class MySqlDatabase extends Database {
} catch (SQLException e) { } catch (SQLException e) {
getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e); getLogger().log(Level.SEVERE, "Failed to insert a user into the database", e);
} }
}))); }));
} }
@Override @Override

View File

@@ -152,9 +152,10 @@ public abstract class EventListener {
// Handle asynchronous disconnection // Handle asynchronous disconnection
lockedPlayers.add(user.uuid); lockedPlayers.add(user.uuid);
CompletableFuture.runAsync(() -> plugin.getRedisManager().setUserServerSwitch(user) CompletableFuture.runAsync(() -> plugin.getRedisManager().setUserServerSwitch(user)
.thenRun(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> .thenRun(() -> user.getUserData(plugin.getLoggingAdapter(), plugin.getSettings()).thenAccept(
optionalUserData.ifPresent(userData -> plugin.getRedisManager().setUserData(user, userData) optionalUserData -> optionalUserData.ifPresent(userData -> plugin.getRedisManager()
.thenRun(() -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT))))) .setUserData(user, userData).thenRun(() -> plugin.getDatabase()
.setUserData(user, userData, DataSaveCause.DISCONNECT)))))
.thenRun(() -> lockedPlayers.remove(user.uuid)).exceptionally(throwable -> { .thenRun(() -> lockedPlayers.remove(user.uuid)).exceptionally(throwable -> {
plugin.getLoggingAdapter().log(Level.SEVERE, plugin.getLoggingAdapter().log(Level.SEVERE,
"An exception occurred handling a player disconnection"); "An exception occurred handling a player disconnection");
@@ -172,7 +173,7 @@ public abstract class EventListener {
if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) { if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) {
return; 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())); userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.WORLD_SAVE).join()));
} }
@@ -217,7 +218,7 @@ public abstract class EventListener {
disabling = true; disabling = true;
plugin.getOnlineUsers().stream().filter(user -> !lockedPlayers.contains(user.uuid)).forEach( 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())); userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join()));
plugin.getDatabase().close(); plugin.getDatabase().close();

View File

@@ -260,16 +260,28 @@ public abstract class OnlineUser extends User {
*/ */
public abstract void showMenu(@NotNull ItemEditorMenu menu); public abstract void showMenu(@NotNull ItemEditorMenu menu);
/**
* Returns true if the player is dead
*
* @return true if the player is dead
*/
public abstract boolean isDead();
/** /**
* Get the player's current {@link UserData} in an {@link Optional} * Get the player's current {@link UserData} in an {@link Optional}
* </p> * <p>
* If the {@code SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES} ConfigOption has been set,
* the user's inventory will only be returned if they are alive
* <p>
* If the user data could not be returned due to an exception, the optional will return empty * 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 * @param logger The logger to use for handling exceptions
* @return the player's current {@link UserData} in an optional; empty if an exception occurs * @return the player's current {@link UserData} in an optional; empty if an exception occurs
*/ */
public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger) { public final CompletableFuture<Optional<UserData>> getUserData(@NotNull Logger logger, @NotNull Settings settings) {
return CompletableFuture.supplyAsync(() -> Optional.of(new UserData(getStatus().join(), getInventory().join(), return CompletableFuture.supplyAsync(() -> Optional.of(new UserData(getStatus().join(),
(settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES)
? getInventory().join() : (isDead() ? new ItemData("") : getInventory().join())),
getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(), getEnderChest().join(), getPotionEffects().join(), getAdvancements().join(),
getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(), getStatistics().join(), getLocation().join(), getPersistentDataContainer().join(),
getMinecraftVersion().toString()))) getMinecraftVersion().toString())))

View File

@@ -1,8 +1,7 @@
package net.william278.husksync.util; package net.william278.husksync.util;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
/** /**
@@ -14,15 +13,8 @@ public interface ResourceReader {
* Gets the resource with given filename and reads it as an {@link InputStream} * Gets the resource with given filename and reads it as an {@link InputStream}
* *
* @param fileName Name of the resource file to read * @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); @Nullable InputStream getResource(String fileName);
/**
* Gets the plugin data folder where plugin configuration and data are kept
*
* @return the plugin data directory
*/
@NotNull File getDataFolder();
} }

View File

@@ -38,6 +38,7 @@ synchronization:
max_user_data_snapshots: 5 max_user_data_snapshots: 5
save_on_world_save: true save_on_world_save: true
compress_data: true compress_data: true
save_dead_player_inventories: true
network_latency_milliseconds: 500 network_latency_milliseconds: 500
features: features:
inventories: true inventories: true

View File

@@ -0,0 +1,31 @@
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⚠ Базирано на статистики от играта)'
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_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%)'

View File

@@ -1,12 +1,16 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import net.william278.husksync.config.Settings;
import net.william278.husksync.logger.DummyLogger; import net.william278.husksync.logger.DummyLogger;
import net.william278.husksync.player.DummyPlayer; import net.william278.husksync.player.DummyPlayer;
import net.william278.husksync.player.OnlineUser; import net.william278.husksync.player.OnlineUser;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
@@ -19,7 +23,7 @@ public class DataAdaptionTests {
public void testJsonDataAdapter() { public void testJsonDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
final AtomicBoolean isEquals = new AtomicBoolean(false); final AtomicBoolean isEquals = new AtomicBoolean(false);
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new JsonDataAdapter(); final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data); final UserData deserializedUserData = dataAdapter.fromBytes(data);
@@ -45,9 +49,9 @@ public class DataAdaptionTests {
@Test @Test
public void testJsonFormat() { public void testJsonFormat() {
final OnlineUser dummyUser = DummyPlayer.create(); 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\":{}},\"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\":2}";
AtomicReference<String> json = new AtomicReference<>(); AtomicReference<String> json = new AtomicReference<>();
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new JsonDataAdapter(); final DataAdapter dataAdapter = new JsonDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
json.set(new String(data, StandardCharsets.UTF_8)); json.set(new String(data, StandardCharsets.UTF_8));
@@ -59,7 +63,7 @@ public class DataAdaptionTests {
public void testCompressedDataAdapter() { public void testCompressedDataAdapter() {
final OnlineUser dummyUser = DummyPlayer.create(); final OnlineUser dummyUser = DummyPlayer.create();
AtomicBoolean isEquals = new AtomicBoolean(false); AtomicBoolean isEquals = new AtomicBoolean(false);
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> { dummyUser.getUserData(new DummyLogger(), DummySettings.get()).join().ifPresent(dummyUserData -> {
final DataAdapter dataAdapter = new CompressedDataAdapter(); final DataAdapter dataAdapter = new CompressedDataAdapter();
final byte[] data = dataAdapter.toBytes(dummyUserData); final byte[] data = dataAdapter.toBytes(dummyUserData);
final UserData deserializedUserData = dataAdapter.fromBytes(data); final UserData deserializedUserData = dataAdapter.fromBytes(data);
@@ -82,4 +86,40 @@ public class DataAdaptionTests {
Assertions.assertTrue(isEquals.get()); 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\":0}");
}
// For testing settings
private static class DummySettings extends Settings {
private DummySettings(@NotNull Map<ConfigOption, Object> settings) {
super(settings);
}
public static DummySettings get() {
return new DummySettings(Map.of(
ConfigOption.SYNCHRONIZATION_SAVE_DEAD_PLAYER_INVENTORIES, true
));
}
}
} }

View File

@@ -155,4 +155,9 @@ public class DummyPlayer extends OnlineUser {
// do nothing // do nothing
} }
@Override
public boolean isDead() {
return false;
}
} }

View File

@@ -3,5 +3,9 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true org.gradle.daemon=true
javaVersion=16 javaVersion=16
plugin_version=2.0.1 plugin_version=2.0.2
plugin_archive=husksync plugin_archive=husksync
jedis_version=4.2.3
mysql_driver_version=8.0.30
snappy_version=1.1.8.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB