mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4663842946 | ||
|
|
e4262abfd7 | ||
|
|
fc6a760848 | ||
|
|
e03a580870 | ||
|
|
112e5fe0bd | ||
|
|
ae4f005a9c | ||
|
|
d1432ebb31 | ||
|
|
460cb54a7d | ||
|
|
ebf5b77f00 | ||
|
|
33904d82d0 | ||
|
|
10b3eb5a43 | ||
|
|
7ae0709895 | ||
|
|
9d6da91a5e | ||
|
|
268c351a95 | ||
|
|
8760fcea1f | ||
|
|
60a3bba165 | ||
|
|
082b3e6c42 | ||
|
|
221baa7b04 | ||
|
|
8b7b32906e |
@@ -15,6 +15,7 @@ import org.bukkit.event.Listener;
|
||||
import org.bukkit.event.block.BlockBreakEvent;
|
||||
import org.bukkit.event.block.BlockPlaceEvent;
|
||||
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;
|
||||
@@ -120,4 +121,11 @@ public class BukkitEventListener extends EventListener implements Listener {
|
||||
}
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = true)
|
||||
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||
if (cancelPlayerEvent(BukkitPlayer.adapt(event.getEntity()))) {
|
||||
event.getDrops().clear();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Bukkit implementation of an {@link OnlineUser}
|
||||
@@ -82,7 +83,12 @@ public class BukkitPlayer extends OnlineUser {
|
||||
if (statusDataFlags.contains(StatusDataFlag.SET_HEALTH)) {
|
||||
final double currentHealth = player.getHealth();
|
||||
if (statusData.health != currentHealth) {
|
||||
player.setHealth(currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health);
|
||||
final double healthToSet = currentHealth > currentMaxHealth ? currentMaxHealth : statusData.health;
|
||||
if (healthToSet <= 0) {
|
||||
Bukkit.getScheduler().runTask(BukkitHuskSync.getInstance(), () -> player.setHealth(healthToSet));
|
||||
} else {
|
||||
player.setHealth(healthToSet);
|
||||
}
|
||||
}
|
||||
|
||||
if (statusData.healthScale != 0d) {
|
||||
@@ -397,10 +403,18 @@ public class BukkitPlayer extends OnlineUser {
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
}
|
||||
final HashMap<String, Byte[]> persistentDataMap = new HashMap<>();
|
||||
for (NamespacedKey key : container.getKeys()) {
|
||||
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY)));
|
||||
// Set persistent data keys; ignore keys that we cannot synchronise as byte arrays
|
||||
for (final NamespacedKey key : container.getKeys()) {
|
||||
try {
|
||||
persistentDataMap.put(key.toString(), ArrayUtils.toObject(container.get(key, PersistentDataType.BYTE_ARRAY)));
|
||||
} catch (IllegalArgumentException | NullPointerException ignored) {
|
||||
}
|
||||
}
|
||||
return new PersistentDataContainerData(persistentDataMap);
|
||||
}).exceptionally(throwable -> {
|
||||
BukkitHuskSync.getInstance().getLoggingAdapter().log(Level.WARNING, "Could not read " + player.getName() + "'s persistent data map, skipping!");
|
||||
throwable.printStackTrace();
|
||||
return new PersistentDataContainerData(new HashMap<>());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -420,11 +434,6 @@ public class BukkitPlayer extends OnlineUser {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return player.getHealth() <= 0d;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
try {
|
||||
|
||||
@@ -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 Optional.of(((OnlineUser) user).getUserData().join());
|
||||
return ((OnlineUser) user).getUserData(plugin.getLoggingAdapter()).join();
|
||||
} else {
|
||||
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||
}
|
||||
@@ -103,8 +103,8 @@ public abstract class BaseHuskSyncAPI {
|
||||
* @since 2.0
|
||||
*/
|
||||
public final CompletableFuture<Void> saveUserData(@NotNull OnlineUser user) {
|
||||
return CompletableFuture.runAsync(() -> user.getUserData().thenAccept(userData ->
|
||||
plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join()));
|
||||
return CompletableFuture.runAsync(() -> user.getUserData(plugin.getLoggingAdapter()).thenAccept(optionalUserData -> optionalUserData.ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.API).join())));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ public class Locales {
|
||||
[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)
|
||||
[• 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)
|
||||
|
||||
@@ -15,7 +15,7 @@ public class UserData {
|
||||
* </p>
|
||||
* This value is to be incremented whenever the format changes.
|
||||
*/
|
||||
private static final int CURRENT_FORMAT_VERSION = 1;
|
||||
public static final int CURRENT_FORMAT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Stores the user's status data, including health, food, etc.
|
||||
@@ -136,4 +136,8 @@ public class UserData {
|
||||
return minecraftVersion;
|
||||
}
|
||||
|
||||
public int getFormatVersion() {
|
||||
return formatVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -29,9 +30,11 @@ public abstract class EventListener {
|
||||
protected final HuskSync plugin;
|
||||
|
||||
/**
|
||||
* Set of UUIDs current awaiting item synchronization. Events will be cancelled for these users
|
||||
* Set of UUIDs of "locked players", for which events will be cancelled.
|
||||
* </p>
|
||||
* Players are locked while their items are being set (on join) or saved (on quit)
|
||||
*/
|
||||
private final HashSet<UUID> usersAwaitingSync;
|
||||
private final Set<UUID> lockedPlayers;
|
||||
|
||||
/**
|
||||
* Whether the plugin is currently being disabled
|
||||
@@ -40,7 +43,7 @@ public abstract class EventListener {
|
||||
|
||||
protected EventListener(@NotNull HuskSync plugin) {
|
||||
this.plugin = plugin;
|
||||
this.usersAwaitingSync = new HashSet<>();
|
||||
this.lockedPlayers = new HashSet<>();
|
||||
this.disabling = false;
|
||||
}
|
||||
|
||||
@@ -50,10 +53,7 @@ public abstract class EventListener {
|
||||
* @param user The {@link OnlineUser} to handle
|
||||
*/
|
||||
protected final void handlePlayerJoin(@NotNull OnlineUser user) {
|
||||
if (user.isDead()) {
|
||||
return;
|
||||
}
|
||||
usersAwaitingSync.add(user.uuid);
|
||||
lockedPlayers.add(user.uuid);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
// Hold reading data for the network latency threshold, to ensure the source server has set the redis key
|
||||
@@ -79,8 +79,8 @@ public abstract class EventListener {
|
||||
}
|
||||
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
|
||||
executor.shutdown();
|
||||
setUserFromDatabase(user)
|
||||
.thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded));
|
||||
setUserFromDatabase(user).thenAccept(
|
||||
succeeded -> handleSynchronisationCompletion(user, succeeded));
|
||||
return;
|
||||
}
|
||||
plugin.getRedisManager().getUserData(user).thenAccept(redisUserData ->
|
||||
@@ -124,7 +124,7 @@ public abstract class EventListener {
|
||||
private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) {
|
||||
if (succeeded) {
|
||||
plugin.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
|
||||
usersAwaitingSync.remove(user.uuid);
|
||||
lockedPlayers.remove(user.uuid);
|
||||
plugin.getDatabase().ensureUser(user).join();
|
||||
plugin.getEventCannon().fireSyncCompleteEvent(user);
|
||||
} else {
|
||||
@@ -145,13 +145,22 @@ public abstract class EventListener {
|
||||
return;
|
||||
}
|
||||
// Don't sync players awaiting synchronization
|
||||
if (usersAwaitingSync.contains(user.uuid)) {
|
||||
if (lockedPlayers.contains(user.uuid)) {
|
||||
return;
|
||||
}
|
||||
plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept(
|
||||
userData -> plugin.getRedisManager().setUserData(user, userData).thenRun(
|
||||
() -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join())));
|
||||
usersAwaitingSync.remove(user.uuid);
|
||||
|
||||
// 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(() -> lockedPlayers.remove(user.uuid)).exceptionally(throwable -> {
|
||||
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||
"An exception occurred handling a player disconnection");
|
||||
throwable.printStackTrace();
|
||||
return null;
|
||||
}).join());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,8 +172,8 @@ public abstract class EventListener {
|
||||
if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) {
|
||||
return;
|
||||
}
|
||||
usersInWorld.forEach(user -> plugin.getDatabase().setUserData(user, user.getUserData().join(),
|
||||
DataSaveCause.WORLD_SAVE).join());
|
||||
usersInWorld.forEach(user -> user.getUserData(plugin.getLoggingAdapter()).join().ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.WORLD_SAVE).join()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +207,7 @@ public abstract class EventListener {
|
||||
* @return Whether the event should be cancelled
|
||||
*/
|
||||
protected final boolean cancelPlayerEvent(@NotNull OnlineUser user) {
|
||||
return disabling || usersAwaitingSync.contains(user.uuid);
|
||||
return disabling || lockedPlayers.contains(user.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,8 +216,9 @@ public abstract class EventListener {
|
||||
public final void handlePluginDisable() {
|
||||
disabling = true;
|
||||
|
||||
plugin.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user ->
|
||||
plugin.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join());
|
||||
plugin.getOnlineUsers().stream().filter(user -> !lockedPlayers.contains(user.uuid)).forEach(
|
||||
user -> user.getUserData(plugin.getLoggingAdapter()).join().ifPresent(
|
||||
userData -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.SERVER_SHUTDOWN).join()));
|
||||
|
||||
plugin.getDatabase().close();
|
||||
plugin.getRedisManager().close();
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
@@ -148,13 +149,6 @@ public abstract class OnlineUser extends User {
|
||||
*/
|
||||
public abstract CompletableFuture<Void> setPersistentDataContainer(@NotNull PersistentDataContainerData persistentDataContainerData);
|
||||
|
||||
/**
|
||||
* Indicates if the player is currently dead
|
||||
*
|
||||
* @return {@code true} if the player is dead ({@code health <= 0}); {@code false} otherwise
|
||||
*/
|
||||
public abstract boolean isDead();
|
||||
|
||||
/**
|
||||
* Indicates if the player has gone offline
|
||||
*
|
||||
@@ -181,17 +175,26 @@ public abstract class OnlineUser extends User {
|
||||
@NotNull EventCannon eventCannon, @NotNull Logger logger,
|
||||
@NotNull Version serverMinecraftVersion) {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
// Prevent synchronizing newer versions of Minecraft
|
||||
// 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 player " + username + " with Minecraft version \""
|
||||
+ data.getMinecraftVersion() + "\" because it is newer than the server's version, \""
|
||||
+ serverMinecraftVersion + "\"");
|
||||
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() && !isDead() && !preSyncEvent.isCancelled()) {
|
||||
if (!isOffline() && !preSyncEvent.isCancelled()) {
|
||||
if (settings.getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SYNC_INVENTORIES)) {
|
||||
add(setInventory(finalData.getInventoryData()));
|
||||
}
|
||||
@@ -258,16 +261,23 @@ public abstract class OnlineUser extends User {
|
||||
public abstract void showMenu(@NotNull ItemEditorMenu menu);
|
||||
|
||||
/**
|
||||
* Get the player's current {@link UserData}
|
||||
* Get the player's current {@link UserData} in an {@link Optional}
|
||||
* </p>
|
||||
* If the user data could not be returned due to an exception, the optional will return empty
|
||||
*
|
||||
* @return the player's current {@link UserData}
|
||||
* @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<UserData> getUserData() {
|
||||
return CompletableFuture.supplyAsync(
|
||||
() -> new UserData(getStatus().join(), getInventory().join(),
|
||||
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()));
|
||||
getMinecraftVersion().toString())))
|
||||
.exceptionally(exception -> {
|
||||
logger.log(Level.SEVERE, "Failed to get user data from online player " + username + " (" + exception.getMessage() + ")");
|
||||
exception.printStackTrace();
|
||||
return Optional.empty();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -165,13 +165,17 @@ public class RedisManager {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.DATA_UPDATE, user.uuid);
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Read " + RedisKeyType.DATA_UPDATE.name()
|
||||
+ " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
final byte[] dataByteArray = jedis.get(key);
|
||||
if (dataByteArray == null) {
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Could not read " +
|
||||
RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return Optional.empty();
|
||||
}
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Successfully read "
|
||||
+ RedisKeyType.DATA_UPDATE.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
|
||||
@@ -188,13 +192,17 @@ public class RedisManager {
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
try (Jedis jedis = jedisPool.getResource()) {
|
||||
final byte[] key = getKey(RedisKeyType.SERVER_SWITCH, user.uuid);
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Read " + RedisKeyType.SERVER_SWITCH.name()
|
||||
+ " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
final byte[] readData = jedis.get(key);
|
||||
if (readData == null) {
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Could not read " +
|
||||
RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
return false;
|
||||
}
|
||||
plugin.getLoggingAdapter().debug("[" + user.username + "] Successfully read "
|
||||
+ RedisKeyType.SERVER_SWITCH.name() + " key from redis at: " +
|
||||
new SimpleDateFormat("mm:ss.SSS").format(new Date()));
|
||||
|
||||
// Consume the key (delete from redis)
|
||||
jedis.del(key);
|
||||
return true;
|
||||
|
||||
@@ -15,7 +15,7 @@ CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
||||
`timestamp` datetime NOT NULL,
|
||||
`save_cause` varchar(32) NOT NULL,
|
||||
`pinned` boolean NOT NULL DEFAULT FALSE,
|
||||
`data` mediumblob NOT NULL,
|
||||
`data` longblob NOT NULL,
|
||||
PRIMARY KEY (`version_uuid`, `player_uuid`),
|
||||
FOREIGN KEY (`player_uuid`) REFERENCES `%users_table%` (`uuid`) ON DELETE CASCADE
|
||||
);
|
||||
@@ -1,31 +1,31 @@
|
||||
synchronisation_complete: '[⏵資料已同步!!](#00fb9a)'
|
||||
synchronisation_failed: '[⏵ Failed to synchronise your data! Please contact an administrator.](#ff7e5e)'
|
||||
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) [該指令只能通過 %1% 控制台運行](#ff7e5e)'
|
||||
error_in_game_command_only: 'Error: That command can only be used in-game.'
|
||||
error_no_data_to_display: '[Error:](#ff3300) [Could not find any user data to display.](#ff7e5e)'
|
||||
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)'
|
||||
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_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_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%)'
|
||||
error_console_command_only: '[錯誤:](#ff3300) [該指令只能透過 控制台 執行](#ff7e5e)'
|
||||
error_in_game_command_only: '[錯誤:](#ff3300) [該指令只能在遊戲內執行](#ff7e5e)'
|
||||
error_no_data_to_display: '[錯誤:](#ff3300) [找不到任何可顯示的用戶資訊.](#ff7e5e)'
|
||||
error_invalid_version_uuid: '[錯誤:](#ff3300) [找不到正確的 Version 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) [%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_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)'
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import net.william278.husksync.logger.DummyLogger;
|
||||
import net.william278.husksync.player.DummyPlayer;
|
||||
import net.william278.husksync.player.OnlineUser;
|
||||
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.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Tests for the data system {@link DataAdapter}
|
||||
@@ -15,64 +18,68 @@ public class DataAdaptionTests {
|
||||
@Test
|
||||
public void testJsonDataAdapter() {
|
||||
final OnlineUser dummyUser = DummyPlayer.create();
|
||||
final UserData dummyUserData = dummyUser.getUserData().join();
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final UserData deserializedUserData = dataAdapter.fromBytes(data);
|
||||
final AtomicBoolean isEquals = new AtomicBoolean(false);
|
||||
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> {
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final UserData deserializedUserData = dataAdapter.fromBytes(data);
|
||||
|
||||
boolean isEquals = 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;
|
||||
|
||||
Assertions.assertTrue(isEquals);
|
||||
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);
|
||||
});
|
||||
Assertions.assertTrue(isEquals.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJsonFormat() {
|
||||
final OnlineUser dummyUser = DummyPlayer.create();
|
||||
final UserData dummyUserData = dummyUser.getUserData().join();
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final String json = new String(data, StandardCharsets.UTF_8);
|
||||
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}";
|
||||
Assertions.assertEquals(expectedJson, json);
|
||||
AtomicReference<String> json = new AtomicReference<>();
|
||||
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> {
|
||||
final DataAdapter dataAdapter = new JsonDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
json.set(new String(data, StandardCharsets.UTF_8));
|
||||
});
|
||||
Assertions.assertEquals(expectedJson, json.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCompressedDataAdapter() {
|
||||
final OnlineUser dummyUser = DummyPlayer.create();
|
||||
final UserData dummyUserData = dummyUser.getUserData().join();
|
||||
final DataAdapter dataAdapter = new CompressedDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final UserData deserializedUserData = dataAdapter.fromBytes(data);
|
||||
AtomicBoolean isEquals = new AtomicBoolean(false);
|
||||
dummyUser.getUserData(new DummyLogger()).join().ifPresent(dummyUserData -> {
|
||||
final DataAdapter dataAdapter = new CompressedDataAdapter();
|
||||
final byte[] data = dataAdapter.toBytes(dummyUserData);
|
||||
final UserData deserializedUserData = dataAdapter.fromBytes(data);
|
||||
|
||||
boolean isEquals = 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;
|
||||
|
||||
Assertions.assertTrue(isEquals);
|
||||
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);
|
||||
});
|
||||
Assertions.assertTrue(isEquals.get());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.william278.husksync.logger;
|
||||
|
||||
import de.themoep.minedown.MineDown;
|
||||
import net.william278.husksync.util.Logger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class DummyLogger extends Logger {
|
||||
|
||||
public DummyLogger() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message, @NotNull Exception e) {
|
||||
System.out.println(level.getName() + ": " + message);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(@NotNull Level level, @NotNull String message) {
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void severe(@NotNull String message) {
|
||||
System.out.println(Level.SEVERE.getName() + ": " + message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void config(@NotNull String message) {
|
||||
System.out.println(Level.CONFIG.getName() + ": " + message);
|
||||
}
|
||||
}
|
||||
@@ -124,11 +124,6 @@ public class DummyPlayer extends OnlineUser {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDead() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOffline() {
|
||||
return false;
|
||||
|
||||
@@ -3,5 +3,5 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
|
||||
org.gradle.daemon=true
|
||||
javaVersion=16
|
||||
|
||||
plugin_version=2.0
|
||||
plugin_version=2.0.1
|
||||
plugin_archive=husksync
|
||||
Reference in New Issue
Block a user