mirror of
https://github.com/WiIIiam278/HuskSync.git
synced 2025-12-23 16:49:19 +00:00
User data pinning, version validation checks, fixes
This commit is contained in:
@@ -77,6 +77,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
|||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
// Process initialization stages
|
// Process initialization stages
|
||||||
CompletableFuture.supplyAsync(() -> {
|
CompletableFuture.supplyAsync(() -> {
|
||||||
|
|
||||||
// Set the logging adapter and resource reader
|
// Set the logging adapter and resource reader
|
||||||
this.logger = new BukkitLogger(this.getLogger());
|
this.logger = new BukkitLogger(this.getLogger());
|
||||||
this.resourceReader = new BukkitResourceReader(this);
|
this.resourceReader = new BukkitResourceReader(this);
|
||||||
@@ -117,12 +118,19 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
|||||||
}).thenApply(succeeded -> {
|
}).thenApply(succeeded -> {
|
||||||
// Prepare migrators
|
// Prepare migrators
|
||||||
if (succeeded) {
|
if (succeeded) {
|
||||||
|
logger.debug("m0");
|
||||||
availableMigrators = new ArrayList<>();
|
availableMigrators = new ArrayList<>();
|
||||||
|
logger.debug("m1");
|
||||||
availableMigrators.add(new LegacyMigrator(this));
|
availableMigrators.add(new LegacyMigrator(this));
|
||||||
|
logger.debug("m2");
|
||||||
final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
final Plugin mySqlPlayerDataBridge = Bukkit.getPluginManager().getPlugin("MySqlPlayerDataBridge");
|
||||||
|
logger.debug("m3");
|
||||||
if (mySqlPlayerDataBridge != null) {
|
if (mySqlPlayerDataBridge != null) {
|
||||||
|
logger.debug("m4");
|
||||||
availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
|
availableMigrators.add(new MpdbMigrator(this, mySqlPlayerDataBridge));
|
||||||
|
logger.debug("m5");
|
||||||
}
|
}
|
||||||
|
logger.debug("m6 - Successfully prepared migrators");
|
||||||
}
|
}
|
||||||
return succeeded;
|
return succeeded;
|
||||||
}).thenApply(succeeded -> {
|
}).thenApply(succeeded -> {
|
||||||
@@ -198,17 +206,22 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
|||||||
// Check for updates
|
// Check for updates
|
||||||
if (succeeded && settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
|
if (succeeded && settings.getBooleanValue(Settings.ConfigOption.CHECK_FOR_UPDATES)) {
|
||||||
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
|
getLoggingAdapter().log(Level.INFO, "Checking for updates...");
|
||||||
new UpdateChecker(getVersion(), getLoggingAdapter()).logToConsole();
|
new UpdateChecker(getPluginVersion(), getLoggingAdapter()).logToConsole();
|
||||||
}
|
}
|
||||||
return succeeded;
|
return succeeded;
|
||||||
}).thenAccept(succeeded -> {
|
}).thenAccept(succeeded -> {
|
||||||
// Handle failed initialization
|
// Handle failed initialization
|
||||||
if (!succeeded) {
|
if (!succeeded) {
|
||||||
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. " + "The plugin will now be disabled");
|
getLoggingAdapter().log(Level.SEVERE, "Failed to initialize HuskSync. The plugin will now be disabled");
|
||||||
getServer().getPluginManager().disablePlugin(this);
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
} else {
|
} else {
|
||||||
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getVersion());
|
getLoggingAdapter().log(Level.INFO, "Successfully enabled HuskSync v" + getPluginVersion());
|
||||||
}
|
}
|
||||||
|
}).exceptionally(exception -> {
|
||||||
|
getLoggingAdapter().log(Level.SEVERE, "An exception occurred initializing HuskSync. (" + exception.getMessage() + ") The plugin will now be disabled.");
|
||||||
|
exception.printStackTrace();
|
||||||
|
getServer().getPluginManager().disablePlugin(this);
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +230,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
|||||||
if (this.eventListener != null) {
|
if (this.eventListener != null) {
|
||||||
this.eventListener.handlePluginDisable();
|
this.eventListener.handlePluginDisable();
|
||||||
}
|
}
|
||||||
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getVersion());
|
getLoggingAdapter().log(Level.INFO, "Successfully disabled HuskSync v" + getPluginVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -281,8 +294,13 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull String getVersion() {
|
public @NotNull Version getPluginVersion() {
|
||||||
return getDescription().getVersion();
|
return Version.pluginVersion(getDescription().getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NotNull Version getMinecraftVersion() {
|
||||||
|
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -53,12 +53,12 @@ public class BukkitEventListener extends EventListener implements Listener {
|
|||||||
public void onInventoryClose(@NotNull InventoryCloseEvent event) {
|
public void onInventoryClose(@NotNull InventoryCloseEvent event) {
|
||||||
if (event.getPlayer() instanceof Player player) {
|
if (event.getPlayer() instanceof Player player) {
|
||||||
final OnlineUser user = BukkitPlayer.adapt(player);
|
final OnlineUser user = BukkitPlayer.adapt(player);
|
||||||
if (huskSync.getDataEditor().isEditingInventoryData(user)) {
|
if (plugin.getDataEditor().isEditingInventoryData(user)) {
|
||||||
try {
|
try {
|
||||||
BukkitSerializer.serializeItemStackArray(event.getInventory().getContents()).thenAccept(
|
BukkitSerializer.serializeItemStackArray(event.getInventory().getContents()).thenAccept(
|
||||||
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
|
serializedInventory -> super.handleMenuClose(user, new ItemData(serializedInventory)));
|
||||||
} catch (DataSerializationException e) {
|
} catch (DataSerializationException e) {
|
||||||
huskSync.getLoggingAdapter().log(Level.SEVERE,
|
plugin.getLoggingAdapter().log(Level.SEVERE,
|
||||||
"Failed to serialize inventory data during menu close", e);
|
"Failed to serialize inventory data during menu close", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public class LegacyMigrator extends Migrator {
|
|||||||
private String sourcePlayersTable;
|
private String sourcePlayersTable;
|
||||||
private String sourceDataTable;
|
private String sourceDataTable;
|
||||||
|
|
||||||
|
private final String minecraftVersion;
|
||||||
|
|
||||||
public LegacyMigrator(@NotNull HuskSync plugin) {
|
public LegacyMigrator(@NotNull HuskSync plugin) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
this.hslConverter = HSLConverter.getInstance();
|
this.hslConverter = HSLConverter.getInstance();
|
||||||
@@ -42,6 +44,7 @@ public class LegacyMigrator extends Migrator {
|
|||||||
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
this.sourceDatabase = plugin.getSettings().getStringValue(Settings.ConfigOption.DATABASE_NAME);
|
||||||
this.sourcePlayersTable = "husksync_players";
|
this.sourcePlayersTable = "husksync_players";
|
||||||
this.sourceDataTable = "husksync_data";
|
this.sourceDataTable = "husksync_data";
|
||||||
|
this.minecraftVersion = plugin.getMinecraftVersion().getWithoutMeta();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,7 +113,7 @@ 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 latest HuskSync user data format...");
|
||||||
dataToMigrate.forEach(data -> data.toUserData(hslConverter).thenAccept(convertedData ->
|
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 -> {
|
||||||
@@ -246,7 +249,8 @@ public class LegacyMigrator extends Migrator {
|
|||||||
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
|
@NotNull String serializedAdvancements, @NotNull String serializedLocation) {
|
||||||
|
|
||||||
@NotNull
|
@NotNull
|
||||||
public CompletableFuture<UserData> toUserData(@NotNull HSLConverter converter) {
|
public CompletableFuture<UserData> toUserData(@NotNull HSLConverter converter,
|
||||||
|
@NotNull String minecraftVersion) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
final DataSerializer.StatisticData legacyStatisticData = converter
|
final DataSerializer.StatisticData legacyStatisticData = converter
|
||||||
@@ -278,7 +282,8 @@ public class LegacyMigrator extends Migrator {
|
|||||||
new ItemData(serializedInventory), new ItemData(serializedEnderChest),
|
new ItemData(serializedInventory), new ItemData(serializedEnderChest),
|
||||||
new PotionEffectData(serializedPotionEffects), convertedAdvancements,
|
new PotionEffectData(serializedPotionEffects), convertedAdvancements,
|
||||||
convertedStatisticData, convertedLocationData,
|
convertedStatisticData, convertedLocationData,
|
||||||
new PersistentDataContainerData(new HashMap<>()));
|
new PersistentDataContainerData(new HashMap<>()),
|
||||||
|
minecraftVersion);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public class MpdbMigrator extends Migrator {
|
|||||||
private String sourceInventoryTable;
|
private String sourceInventoryTable;
|
||||||
private String sourceEnderChestTable;
|
private String sourceEnderChestTable;
|
||||||
private String sourceExperienceTable;
|
private String sourceExperienceTable;
|
||||||
|
private final String minecraftVersion;
|
||||||
|
|
||||||
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
|
public MpdbMigrator(@NotNull BukkitHuskSync plugin, @NotNull Plugin mySqlPlayerDataBridge) {
|
||||||
super(plugin);
|
super(plugin);
|
||||||
@@ -47,6 +48,8 @@ public class MpdbMigrator extends Migrator {
|
|||||||
this.sourceInventoryTable = "mpdb_inventory";
|
this.sourceInventoryTable = "mpdb_inventory";
|
||||||
this.sourceEnderChestTable = "mpdb_enderchest";
|
this.sourceEnderChestTable = "mpdb_enderchest";
|
||||||
this.sourceExperienceTable = "mpdb_experience";
|
this.sourceExperienceTable = "mpdb_experience";
|
||||||
|
this.minecraftVersion = plugin.getMinecraftVersion().getWithoutMeta();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -106,7 +109,7 @@ 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...");
|
||||||
dataToMigrate.forEach(data -> data.toUserData(mpdbConverter).thenAccept(convertedData ->
|
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 -> {
|
||||||
@@ -257,7 +260,8 @@ public class MpdbMigrator extends Migrator {
|
|||||||
* @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object
|
* @return A {@link CompletableFuture} that will resolve to the converted {@link UserData} object
|
||||||
*/
|
*/
|
||||||
@NotNull
|
@NotNull
|
||||||
public CompletableFuture<UserData> toUserData(@NotNull MPDBConverter converter) {
|
public CompletableFuture<UserData> toUserData(@NotNull MPDBConverter converter,
|
||||||
|
@NotNull String minecraftVersion) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
// Combine inventory and armour
|
// Combine inventory and armour
|
||||||
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
final Inventory inventory = Bukkit.createInventory(null, InventoryType.PLAYER);
|
||||||
@@ -278,7 +282,8 @@ public class MpdbMigrator extends Migrator {
|
|||||||
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()),
|
new StatisticsData(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()),
|
||||||
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
|
new LocationData("world", UUID.randomUUID(), "NORMAL", 0, 0, 0,
|
||||||
0f, 0f),
|
0f, 0f),
|
||||||
new PersistentDataContainerData(new HashMap<>()));
|
new PersistentDataContainerData(new HashMap<>()),
|
||||||
|
minecraftVersion);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import net.md_5.bungee.api.chat.BaseComponent;
|
|||||||
import net.william278.husksync.BukkitHuskSync;
|
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 org.apache.commons.lang.ArrayUtils;
|
import org.apache.commons.lang.ArrayUtils;
|
||||||
import org.bukkit.*;
|
import org.bukkit.*;
|
||||||
import org.bukkit.advancement.Advancement;
|
import org.bukkit.advancement.Advancement;
|
||||||
@@ -434,6 +435,12 @@ public class BukkitPlayer extends OnlineUser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public Version getMinecraftVersion() {
|
||||||
|
return Version.minecraftVersion(Bukkit.getBukkitVersion());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPermission(@NotNull String node) {
|
public boolean hasPermission(@NotNull String node) {
|
||||||
return player.hasPermission(node);
|
return player.hasPermission(node);
|
||||||
|
|||||||
@@ -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.Version;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -18,32 +19,122 @@ import java.util.Set;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract implementation of the HuskSync plugin.
|
||||||
|
*/
|
||||||
public interface HuskSync {
|
public interface HuskSync {
|
||||||
|
|
||||||
@NotNull Set<OnlineUser> getOnlineUsers();
|
/**
|
||||||
|
* Returns a set of online players.
|
||||||
|
*
|
||||||
|
* @return a set of online players as {@link OnlineUser}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Set<OnlineUser> getOnlineUsers();
|
||||||
|
|
||||||
@NotNull Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid);
|
/**
|
||||||
|
* Returns an online user by UUID if they exist
|
||||||
|
*
|
||||||
|
* @param uuid the UUID of the user to get
|
||||||
|
* @return an online user as {@link OnlineUser}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid);
|
||||||
|
|
||||||
@NotNull Database getDatabase();
|
/**
|
||||||
|
* Returns the database implementation
|
||||||
|
*
|
||||||
|
* @return the {@link Database} implementation
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Database getDatabase();
|
||||||
|
|
||||||
@NotNull RedisManager getRedisManager();
|
/**
|
||||||
|
* Returns the redis manager implementation
|
||||||
|
*
|
||||||
|
* @return the {@link RedisManager} implementation
|
||||||
|
*/
|
||||||
|
|
||||||
@NotNull DataAdapter getDataAdapter();
|
@NotNull
|
||||||
|
RedisManager getRedisManager();
|
||||||
|
|
||||||
@NotNull DataEditor getDataEditor();
|
/**
|
||||||
|
* Returns the data adapter implementation
|
||||||
|
*
|
||||||
|
* @return the {@link DataAdapter} implementation
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
DataAdapter getDataAdapter();
|
||||||
|
|
||||||
@NotNull EventCannon getEventCannon();
|
/**
|
||||||
|
* Returns the data editor implementation
|
||||||
|
*
|
||||||
|
* @return the {@link DataEditor} implementation
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
DataEditor getDataEditor();
|
||||||
|
|
||||||
@NotNull List<Migrator> getAvailableMigrators();
|
/**
|
||||||
|
* Returns the event firing cannon
|
||||||
|
*
|
||||||
|
* @return the {@link EventCannon} implementation
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
EventCannon getEventCannon();
|
||||||
|
|
||||||
@NotNull Settings getSettings();
|
/**
|
||||||
|
* Returns a list of available data {@link Migrator}s
|
||||||
|
*
|
||||||
|
* @return a list of {@link Migrator}s
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
List<Migrator> getAvailableMigrators();
|
||||||
|
|
||||||
@NotNull Locales getLocales();
|
/**
|
||||||
|
* Returns the plugin {@link Settings}
|
||||||
|
*
|
||||||
|
* @return the {@link Settings}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Settings getSettings();
|
||||||
|
|
||||||
@NotNull Logger getLoggingAdapter();
|
/**
|
||||||
|
* Returns the plugin {@link Locales}
|
||||||
|
*
|
||||||
|
* @return the {@link Locales}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Locales getLocales();
|
||||||
|
|
||||||
@NotNull String getVersion();
|
/**
|
||||||
|
* Returns the plugin {@link Logger}
|
||||||
|
*
|
||||||
|
* @return the {@link Logger}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Logger getLoggingAdapter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the plugin version
|
||||||
|
*
|
||||||
|
* @return the plugin {@link Version}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Version getPluginVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the Minecraft version implementation
|
||||||
|
*
|
||||||
|
* @return the Minecraft {@link Version}
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
Version getMinecraftVersion();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the {@link Settings} and {@link Locales} from their respective config files
|
||||||
|
*
|
||||||
|
* @return a {@link CompletableFuture} that will be completed when the plugin reload is complete and if it was successful
|
||||||
|
*/
|
||||||
CompletableFuture<Boolean> reload();
|
CompletableFuture<Boolean> reload();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package net.william278.husksync.api;
|
|||||||
import net.william278.husksync.HuskSync;
|
import net.william278.husksync.HuskSync;
|
||||||
import net.william278.husksync.data.DataSaveCause;
|
import net.william278.husksync.data.DataSaveCause;
|
||||||
import net.william278.husksync.data.UserData;
|
import net.william278.husksync.data.UserData;
|
||||||
import net.william278.husksync.data.VersionedUserData;
|
import net.william278.husksync.data.UserDataSnapshot;
|
||||||
import net.william278.husksync.player.OnlineUser;
|
import net.william278.husksync.player.OnlineUser;
|
||||||
import net.william278.husksync.player.User;
|
import net.william278.husksync.player.User;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -74,7 +74,7 @@ public abstract class BaseHuskSyncAPI {
|
|||||||
if (user instanceof OnlineUser) {
|
if (user instanceof OnlineUser) {
|
||||||
return Optional.of(((OnlineUser) user).getUserData().join());
|
return Optional.of(((OnlineUser) user).getUserData().join());
|
||||||
} else {
|
} else {
|
||||||
return plugin.getDatabase().getCurrentUserData(user).join().map(VersionedUserData::userData);
|
return plugin.getDatabase().getCurrentUserData(user).join().map(UserDataSnapshot::userData);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -108,16 +108,16 @@ public abstract class BaseHuskSyncAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the saved {@link VersionedUserData} records for the given {@link User}
|
* Returns the saved {@link UserDataSnapshot} records for the given {@link User}
|
||||||
*
|
*
|
||||||
* @param user the {@link User} to get the {@link VersionedUserData} for
|
* @param user the {@link User} to get the {@link UserDataSnapshot} for
|
||||||
* @return future returning a list {@link VersionedUserData} for the given {@link User} if they exist,
|
* @return future returning a list {@link UserDataSnapshot} for the given {@link User} if they exist,
|
||||||
* otherwise an empty {@link Optional}
|
* otherwise an empty {@link Optional}
|
||||||
* @apiNote The length of the list of VersionedUserData will correspond to the configured
|
* @apiNote The length of the list of VersionedUserData will correspond to the configured
|
||||||
* {@code max_user_data_records} config option
|
* {@code max_user_data_records} config option
|
||||||
* @since 2.0
|
* @since 2.0
|
||||||
*/
|
*/
|
||||||
public final CompletableFuture<List<VersionedUserData>> getSavedUserData(@NotNull User user) {
|
public final CompletableFuture<List<UserDataSnapshot>> getSavedUserData(@NotNull User user) {
|
||||||
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
|
return CompletableFuture.supplyAsync(() -> plugin.getDatabase().getUserData(user).join());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import net.william278.husksync.HuskSync;
|
|||||||
import net.william278.husksync.data.DataSaveCause;
|
import net.william278.husksync.data.DataSaveCause;
|
||||||
import net.william278.husksync.data.ItemData;
|
import net.william278.husksync.data.ItemData;
|
||||||
import net.william278.husksync.data.UserData;
|
import net.william278.husksync.data.UserData;
|
||||||
import net.william278.husksync.data.VersionedUserData;
|
import net.william278.husksync.data.UserDataSnapshot;
|
||||||
import net.william278.husksync.editor.ItemEditorMenu;
|
import net.william278.husksync.editor.ItemEditorMenu;
|
||||||
import net.william278.husksync.player.OnlineUser;
|
import net.william278.husksync.player.OnlineUser;
|
||||||
import net.william278.husksync.player.User;
|
import net.william278.husksync.player.User;
|
||||||
@@ -55,15 +55,15 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
|
|||||||
.ifPresent(player::sendMessage)));
|
.ifPresent(player::sendMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull VersionedUserData versionedUserData,
|
private void showEnderChestMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||||
@NotNull User dataOwner, final boolean allowEdit) {
|
@NotNull User dataOwner, final boolean allowEdit) {
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
final UserData data = versionedUserData.userData();
|
final UserData data = userDataSnapshot.userData();
|
||||||
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
|
final ItemEditorMenu menu = ItemEditorMenu.createEnderChestMenu(data.getEnderChestData(),
|
||||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||||
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
|
plugin.getLocales().getLocale("viewing_ender_chest_of", dataOwner.username,
|
||||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||||
.format(versionedUserData.versionTimestamp()))
|
.format(userDataSnapshot.versionTimestamp()))
|
||||||
.ifPresent(player::sendMessage);
|
.ifPresent(player::sendMessage);
|
||||||
final ItemData enderChestDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join();
|
final ItemData enderChestDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join();
|
||||||
if (!menu.canEdit) {
|
if (!menu.canEdit) {
|
||||||
@@ -72,7 +72,8 @@ public class EnderChestCommand extends CommandBase implements TabCompletable {
|
|||||||
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
|
final UserData updatedUserData = new UserData(data.getStatusData(), data.getInventoryData(),
|
||||||
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
|
enderChestDataOnClose, data.getPotionEffectsData(), data.getAdvancementData(),
|
||||||
data.getStatisticsData(), data.getLocationData(),
|
data.getStatisticsData(), data.getLocationData(),
|
||||||
data.getPersistentDataContainerData());
|
data.getPersistentDataContainerData(),
|
||||||
|
plugin.getMinecraftVersion().getWithoutMeta());
|
||||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDER_CHEST_COMMAND_EDIT).join();
|
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.ENDER_CHEST_COMMAND_EDIT).join();
|
||||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
|||||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final UpdateChecker updateChecker = new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter());
|
final UpdateChecker updateChecker = new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter());
|
||||||
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
|
updateChecker.fetchLatestVersion().thenAccept(latestVersion -> {
|
||||||
if (updateChecker.isUpdateAvailable(latestVersion)) {
|
if (updateChecker.isUpdateAvailable(latestVersion)) {
|
||||||
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" +
|
player.sendMessage(new MineDown("[HuskSync](#00fb9a bold) [| A new update is available:](#00fb9a) [HuskSync " + updateChecker.fetchLatestVersion() + "](#00fb9a bold)" +
|
||||||
@@ -57,7 +57,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
|||||||
case "migrate" ->
|
case "migrate" ->
|
||||||
plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage);
|
plugin.getLocales().getLocale("error_console_command_only").ifPresent(player::sendMessage);
|
||||||
default -> plugin.getLocales().getLocale("error_invalid_syntax",
|
default -> plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
"/husksync <update/info/reload>")
|
"/husksync <update/about/reload>")
|
||||||
.ifPresent(player::sendMessage);
|
.ifPresent(player::sendMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,14 +65,14 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
|||||||
@Override
|
@Override
|
||||||
public void onConsoleExecute(@NotNull String[] args) {
|
public void onConsoleExecute(@NotNull String[] args) {
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/info/reload/migrate>\"");
|
plugin.getLoggingAdapter().log(Level.INFO, "Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
switch (args[0].toLowerCase()) {
|
switch (args[0].toLowerCase()) {
|
||||||
case "update", "version" ->
|
case "update", "version" ->
|
||||||
new UpdateChecker(plugin.getVersion(), plugin.getLoggingAdapter()).logToConsole();
|
new UpdateChecker(plugin.getPluginVersion(), plugin.getLoggingAdapter()).logToConsole();
|
||||||
case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown(
|
case "info", "about" -> plugin.getLoggingAdapter().log(Level.INFO, plugin.getLocales().stripMineDown(
|
||||||
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
|
Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString())));
|
||||||
case "reload" -> {
|
case "reload" -> {
|
||||||
plugin.reload();
|
plugin.reload();
|
||||||
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files.");
|
plugin.getLoggingAdapter().log(Level.INFO, "Reloaded config & message files.");
|
||||||
@@ -105,7 +105,7 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
default -> plugin.getLoggingAdapter().log(Level.INFO,
|
default -> plugin.getLoggingAdapter().log(Level.INFO,
|
||||||
"Invalid syntax. Console usage: \"husksync <update/info/reload/migrate>\"");
|
"Invalid syntax. Console usage: \"husksync <update/about/reload/migrate>\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +129,6 @@ public class HuskSyncCommand extends CommandBase implements TabCompletable, Cons
|
|||||||
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
plugin.getLocales().getLocale("error_no_permission").ifPresent(player::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getVersion())));
|
player.sendMessage(new MineDown(Locales.PLUGIN_INFORMATION.replace("%version%", plugin.getPluginVersion().toString())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import net.william278.husksync.HuskSync;
|
|||||||
import net.william278.husksync.data.DataSaveCause;
|
import net.william278.husksync.data.DataSaveCause;
|
||||||
import net.william278.husksync.data.ItemData;
|
import net.william278.husksync.data.ItemData;
|
||||||
import net.william278.husksync.data.UserData;
|
import net.william278.husksync.data.UserData;
|
||||||
import net.william278.husksync.data.VersionedUserData;
|
import net.william278.husksync.data.UserDataSnapshot;
|
||||||
import net.william278.husksync.editor.ItemEditorMenu;
|
import net.william278.husksync.editor.ItemEditorMenu;
|
||||||
import net.william278.husksync.player.OnlineUser;
|
import net.william278.husksync.player.OnlineUser;
|
||||||
import net.william278.husksync.player.User;
|
import net.william278.husksync.player.User;
|
||||||
@@ -55,15 +55,15 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
|
|||||||
.ifPresent(player::sendMessage)));
|
.ifPresent(player::sendMessage)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull VersionedUserData versionedUserData,
|
private void showInventoryMenu(@NotNull OnlineUser player, @NotNull UserDataSnapshot userDataSnapshot,
|
||||||
@NotNull User dataOwner, boolean allowEdit) {
|
@NotNull User dataOwner, boolean allowEdit) {
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
final UserData data = versionedUserData.userData();
|
final UserData data = userDataSnapshot.userData();
|
||||||
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
|
final ItemEditorMenu menu = ItemEditorMenu.createInventoryMenu(data.getInventoryData(),
|
||||||
dataOwner, player, plugin.getLocales(), allowEdit);
|
dataOwner, player, plugin.getLocales(), allowEdit);
|
||||||
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
|
plugin.getLocales().getLocale("viewing_inventory_of", dataOwner.username,
|
||||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||||
.format(versionedUserData.versionTimestamp()))
|
.format(userDataSnapshot.versionTimestamp()))
|
||||||
.ifPresent(player::sendMessage);
|
.ifPresent(player::sendMessage);
|
||||||
final ItemData inventoryDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join();
|
final ItemData inventoryDataOnClose = plugin.getDataEditor().openItemEditorMenu(player, menu).join();
|
||||||
if (!menu.canEdit) {
|
if (!menu.canEdit) {
|
||||||
@@ -72,7 +72,8 @@ public class InventoryCommand extends CommandBase implements TabCompletable {
|
|||||||
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
|
final UserData updatedUserData = new UserData(data.getStatusData(), inventoryDataOnClose,
|
||||||
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
|
data.getEnderChestData(), data.getPotionEffectsData(), data.getAdvancementData(),
|
||||||
data.getStatisticsData(), data.getLocationData(),
|
data.getStatisticsData(), data.getLocationData(),
|
||||||
data.getPersistentDataContainerData());
|
data.getPersistentDataContainerData(),
|
||||||
|
plugin.getMinecraftVersion().getWithoutMeta());
|
||||||
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND_EDIT).join();
|
plugin.getDatabase().setUserData(dataOwner, updatedUserData, DataSaveCause.INVENTORY_COMMAND_EDIT).join();
|
||||||
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
plugin.getRedisManager().sendUserDataUpdate(dataOwner, updatedUserData).join();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class UserDataCommand extends CommandBase implements TabCompletable {
|
public class UserDataCommand extends CommandBase implements TabCompletable {
|
||||||
|
|
||||||
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore"};
|
private final String[] COMMAND_ARGUMENTS = {"view", "list", "delete", "restore", "pin"};
|
||||||
|
|
||||||
public UserDataCommand(@NotNull HuskSync implementor) {
|
public UserDataCommand(@NotNull HuskSync implementor) {
|
||||||
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
|
super("userdata", Permission.COMMAND_USER_DATA, implementor, "playerdata");
|
||||||
@@ -24,7 +24,7 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
|||||||
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
public void onExecute(@NotNull OnlineUser player, @NotNull String[] args) {
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
plugin.getLocales().getLocale("error_invalid_syntax",
|
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
"/userdata <view|list|delete|restore> <username> [version_uuid]")
|
"/userdata <view/list/delete/restore/pin> <username> [version_uuid]")
|
||||||
.ifPresent(player::sendMessage);
|
.ifPresent(player::sendMessage);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -160,6 +160,47 @@ public class UserDataCommand extends CommandBase implements TabCompletable {
|
|||||||
.ifPresent(player::sendMessage);
|
.ifPresent(player::sendMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case "pin" -> {
|
||||||
|
if (args.length < 3) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
|
"/userdata pin <username> <version_uuid>")
|
||||||
|
.ifPresent(player::sendMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String username = args[1];
|
||||||
|
try {
|
||||||
|
final UUID versionUuid = UUID.fromString(args[2]);
|
||||||
|
CompletableFuture.runAsync(() -> plugin.getDatabase().getUserByName(username.toLowerCase()).thenAccept(
|
||||||
|
optionalUser -> optionalUser.ifPresentOrElse(
|
||||||
|
user -> plugin.getDatabase().getUserData(user, versionUuid).thenAccept(
|
||||||
|
optionalUserData -> optionalUserData.ifPresentOrElse(userData -> {
|
||||||
|
if (userData.pinned()) {
|
||||||
|
plugin.getDatabase().unpinUserData(user, versionUuid).join();
|
||||||
|
plugin.getLocales().getLocale("data_unpinned",
|
||||||
|
versionUuid.toString().split("-")[0],
|
||||||
|
versionUuid.toString(),
|
||||||
|
user.username,
|
||||||
|
user.uuid.toString())
|
||||||
|
.ifPresent(player::sendMessage);
|
||||||
|
} else {
|
||||||
|
plugin.getDatabase().pinUserData(user, versionUuid).join();
|
||||||
|
plugin.getLocales().getLocale("data_pinned",
|
||||||
|
versionUuid.toString().split("-")[0],
|
||||||
|
versionUuid.toString(),
|
||||||
|
user.username,
|
||||||
|
user.uuid.toString())
|
||||||
|
.ifPresent(player::sendMessage);
|
||||||
|
}
|
||||||
|
}, () -> plugin.getLocales().getLocale("error_invalid_version_uuid")
|
||||||
|
.ifPresent(player::sendMessage))),
|
||||||
|
() -> plugin.getLocales().getLocale("error_invalid_player")
|
||||||
|
.ifPresent(player::sendMessage))));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
plugin.getLocales().getLocale("error_invalid_syntax",
|
||||||
|
"/userdata pin <username> <version_uuid>")
|
||||||
|
.ifPresent(player::sendMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public class Settings {
|
|||||||
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
|
CHECK_FOR_UPDATES("check_for_updates", OptionType.BOOLEAN, true),
|
||||||
|
|
||||||
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
|
CLUSTER_ID("cluster_id", OptionType.STRING, ""),
|
||||||
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, true),
|
DEBUG_LOGGING("debug_logging", OptionType.BOOLEAN, false),
|
||||||
|
|
||||||
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
|
DATABASE_HOST("database.credentials.host", OptionType.STRING, "localhost"),
|
||||||
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
|
DATABASE_PORT("database.credentials.port", OptionType.INTEGER, 3306),
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ public class UserData {
|
|||||||
@SerializedName("persistent_data_container")
|
@SerializedName("persistent_data_container")
|
||||||
protected PersistentDataContainerData persistentDataContainerData;
|
protected PersistentDataContainerData persistentDataContainerData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the version of Minecraft this data was generated in
|
||||||
|
*/
|
||||||
|
@SerializedName("minecraft_version")
|
||||||
|
protected String minecraftVersion;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores the version of the data format being used
|
* Stores the version of the data format being used
|
||||||
*/
|
*/
|
||||||
@@ -74,7 +80,8 @@ public class UserData {
|
|||||||
public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData,
|
public UserData(@NotNull StatusData statusData, @NotNull ItemData inventoryData,
|
||||||
@NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData,
|
@NotNull ItemData enderChestData, @NotNull PotionEffectData potionEffectData,
|
||||||
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
|
@NotNull List<AdvancementData> advancementData, @NotNull StatisticsData statisticData,
|
||||||
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData) {
|
@NotNull LocationData locationData, @NotNull PersistentDataContainerData persistentDataContainerData,
|
||||||
|
@NotNull String minecraftVersion) {
|
||||||
this.statusData = statusData;
|
this.statusData = statusData;
|
||||||
this.inventoryData = inventoryData;
|
this.inventoryData = inventoryData;
|
||||||
this.enderChestData = enderChestData;
|
this.enderChestData = enderChestData;
|
||||||
@@ -83,6 +90,7 @@ public class UserData {
|
|||||||
this.statisticData = statisticData;
|
this.statisticData = statisticData;
|
||||||
this.locationData = locationData;
|
this.locationData = locationData;
|
||||||
this.persistentDataContainerData = persistentDataContainerData;
|
this.persistentDataContainerData = persistentDataContainerData;
|
||||||
|
this.minecraftVersion = minecraftVersion;
|
||||||
this.formatVersion = CURRENT_FORMAT_VERSION;
|
this.formatVersion = CURRENT_FORMAT_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,4 +131,9 @@ public class UserData {
|
|||||||
return persistentDataContainerData;
|
return persistentDataContainerData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String getMinecraftVersion() {
|
||||||
|
return minecraftVersion;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,21 +13,23 @@ import java.util.UUID;
|
|||||||
* @param userData The {@link UserData} that has been versioned
|
* @param userData The {@link UserData} that has been versioned
|
||||||
* @param cause The {@link DataSaveCause} that caused this data to be saved
|
* @param cause The {@link DataSaveCause} that caused this data to be saved
|
||||||
*/
|
*/
|
||||||
public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
|
public record UserDataSnapshot(@NotNull UUID versionUUID, @NotNull Date versionTimestamp,
|
||||||
@NotNull DataSaveCause cause, @NotNull UserData userData) implements Comparable<VersionedUserData> {
|
@NotNull DataSaveCause cause, boolean pinned,
|
||||||
|
@NotNull UserData userData) implements Comparable<UserDataSnapshot> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Version {@link UserData} into a {@link VersionedUserData}, assigning it a random {@link UUID} and the current timestamp {@link Date}
|
* Version {@link UserData} into a {@link UserDataSnapshot}, assigning it a random {@link UUID} and the current timestamp {@link Date}
|
||||||
* </p>
|
* </p>
|
||||||
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
|
* Note that this method will set {@code cause} to {@link DataSaveCause#API}
|
||||||
*
|
*
|
||||||
* @param userData The {@link UserData} to version
|
* @param userData The {@link UserData} to version
|
||||||
* @return A new {@link VersionedUserData}
|
* @return A new {@link UserDataSnapshot}
|
||||||
* @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.<p>
|
* @implNote This isn't used to version data that is going to be set to a database to prevent UUID collisions.<p>
|
||||||
* Database implementations should instead use their own UUID generation functions.
|
* Database implementations should instead use their own UUID generation functions.
|
||||||
*/
|
*/
|
||||||
public static VersionedUserData version(@NotNull UserData userData) {
|
public static UserDataSnapshot create(@NotNull UserData userData) {
|
||||||
return new VersionedUserData(UUID.randomUUID(), new Date(), DataSaveCause.API, userData);
|
return new UserDataSnapshot(UUID.randomUUID(), new Date(),
|
||||||
|
DataSaveCause.API, false, userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +39,7 @@ public record VersionedUserData(@NotNull UUID versionUUID, @NotNull Date version
|
|||||||
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
* @return the comparison result; the more recent UserData is greater than the less recent UserData
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(@NotNull VersionedUserData other) {
|
public int compareTo(@NotNull UserDataSnapshot other) {
|
||||||
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
|
return Long.compare(this.versionTimestamp.getTime(), other.versionTimestamp.getTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ package net.william278.husksync.database;
|
|||||||
import net.william278.husksync.data.DataAdapter;
|
import net.william278.husksync.data.DataAdapter;
|
||||||
import net.william278.husksync.data.DataSaveCause;
|
import net.william278.husksync.data.DataSaveCause;
|
||||||
import net.william278.husksync.data.UserData;
|
import net.william278.husksync.data.UserData;
|
||||||
import net.william278.husksync.data.VersionedUserData;
|
import net.william278.husksync.data.UserDataSnapshot;
|
||||||
import net.william278.husksync.event.EventCannon;
|
import net.william278.husksync.event.EventCannon;
|
||||||
import net.william278.husksync.migrator.Migrator;
|
import net.william278.husksync.migrator.Migrator;
|
||||||
import net.william278.husksync.player.User;
|
import net.william278.husksync.player.User;
|
||||||
@@ -157,40 +157,41 @@ public abstract class Database {
|
|||||||
* Get the current uniquely versioned user data for a given user, if it exists.
|
* Get the current uniquely versioned user data for a given user, if it exists.
|
||||||
*
|
*
|
||||||
* @param user the user to get data for
|
* @param user the user to get data for
|
||||||
* @return an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not
|
* @return an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||||
*/
|
*/
|
||||||
public abstract CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull User user);
|
public abstract CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all {@link VersionedUserData} entries for a user from the database.
|
* Get all {@link UserDataSnapshot} entries for a user from the database.
|
||||||
*
|
*
|
||||||
* @param user The user to get data for
|
* @param user The user to get data for
|
||||||
* @return A future returning a list of a user's {@link VersionedUserData} entries
|
* @return A future returning a list of a user's {@link UserDataSnapshot} entries
|
||||||
*/
|
*/
|
||||||
public abstract CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user);
|
public abstract CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a specific {@link VersionedUserData} entry for a user from the database, by its UUID.
|
* Gets a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||||
*
|
*
|
||||||
* @param user The user to get data for
|
* @param user The user to get data for
|
||||||
* @param versionUuid The UUID of the {@link VersionedUserData} entry to get
|
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to get
|
||||||
* @return A future returning an optional containing the {@link VersionedUserData}, if it exists, or an empty optional if it does not
|
* @return A future returning an optional containing the {@link UserDataSnapshot}, if it exists, or an empty optional if it does not
|
||||||
*/
|
*/
|
||||||
public abstract CompletableFuture<Optional<VersionedUserData>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
|
public abstract CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured
|
* <b>(Internal)</b> Prune user data for a given user to the maximum value as configured.
|
||||||
*
|
*
|
||||||
* @param user The user to prune data for
|
* @param user The user to prune data for
|
||||||
* @return A future returning void when complete
|
* @return A future returning void when complete
|
||||||
|
* @implNote Data snapshots marked as {@code pinned} are exempt from rotation
|
||||||
*/
|
*/
|
||||||
protected abstract CompletableFuture<Void> pruneUserData(@NotNull User user);
|
protected abstract CompletableFuture<Void> rotateUserData(@NotNull User user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a specific {@link VersionedUserData} entry for a user from the database, by its UUID.
|
* Deletes a specific {@link UserDataSnapshot} entry for a user from the database, by its UUID.
|
||||||
*
|
*
|
||||||
* @param user The user to get data for
|
* @param user The user to get data for
|
||||||
* @param versionUuid The UUID of the {@link VersionedUserData} entry to delete
|
* @param versionUuid The UUID of the {@link UserDataSnapshot} entry to delete
|
||||||
* @return A future returning void when complete
|
* @return A future returning void when complete
|
||||||
*/
|
*/
|
||||||
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
|
public abstract CompletableFuture<Boolean> deleteUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||||
@@ -202,10 +203,30 @@ public abstract class Database {
|
|||||||
* @param user The user to add data for
|
* @param user The user to add data for
|
||||||
* @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion.
|
* @param userData The {@link UserData} to set. The implementation should version it with a random UUID and the current timestamp during insertion.
|
||||||
* @return A future returning void when complete
|
* @return A future returning void when complete
|
||||||
* @see VersionedUserData#version(UserData)
|
* @see UserDataSnapshot#create(UserData)
|
||||||
*/
|
*/
|
||||||
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
|
public abstract CompletableFuture<Void> setUserData(@NotNull User user, @NotNull UserData userData, @NotNull DataSaveCause dataSaveCause);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code true}.
|
||||||
|
*
|
||||||
|
* @param user The user to pin the data for
|
||||||
|
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to pin
|
||||||
|
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||||
|
* @see UserDataSnapshot#pinned()
|
||||||
|
*/
|
||||||
|
public abstract CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unpin a saved {@link UserDataSnapshot} by given version UUID, setting it's {@code pinned} state to {@code false}.
|
||||||
|
*
|
||||||
|
* @param user The user to unpin the data for
|
||||||
|
* @param versionUuid The UUID of the user's {@link UserDataSnapshot} entry to unpin
|
||||||
|
* @return A future returning a boolean; {@code true} if the operation completed successfully, {@code false} if it failed
|
||||||
|
* @see UserDataSnapshot#pinned()
|
||||||
|
*/
|
||||||
|
public abstract CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wipes <b>all</b> {@link UserData} entries from the database.
|
* Wipes <b>all</b> {@link UserData} entries from the database.
|
||||||
* <b>This should never be used</b>, except when preparing tables for migration.
|
* <b>This should never be used</b>, except when preparing tables for migration.
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import org.jetbrains.annotations.NotNull;
|
|||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.sql.*;
|
import java.sql.*;
|
||||||
import java.util.*;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
|
||||||
@@ -206,11 +206,11 @@ public class MySqlDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull User user) {
|
public CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull User user) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
|
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||||
FROM `%user_data_table%`
|
FROM `%user_data_table%`
|
||||||
WHERE `player_uuid`=?
|
WHERE `player_uuid`=?
|
||||||
ORDER BY `timestamp` DESC
|
ORDER BY `timestamp` DESC
|
||||||
@@ -221,10 +221,11 @@ public class MySqlDatabase extends Database {
|
|||||||
final Blob blob = resultSet.getBlob("data");
|
final Blob blob = resultSet.getBlob("data");
|
||||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||||
blob.free();
|
blob.free();
|
||||||
return Optional.of(new VersionedUserData(
|
return Optional.of(new UserDataSnapshot(
|
||||||
UUID.fromString(resultSet.getString("version_uuid")),
|
UUID.fromString(resultSet.getString("version_uuid")),
|
||||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||||
|
resultSet.getBoolean("pinned"),
|
||||||
getDataAdapter().fromBytes(dataByteArray)));
|
getDataAdapter().fromBytes(dataByteArray)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,12 +237,12 @@ public class MySqlDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<List<VersionedUserData>> getUserData(@NotNull User user) {
|
public CompletableFuture<List<UserDataSnapshot>> getUserData(@NotNull User user) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
final List<VersionedUserData> retrievedData = new ArrayList<>();
|
final List<UserDataSnapshot> retrievedData = new ArrayList<>();
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
|
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||||
FROM `%user_data_table%`
|
FROM `%user_data_table%`
|
||||||
WHERE `player_uuid`=?
|
WHERE `player_uuid`=?
|
||||||
ORDER BY `timestamp` DESC;"""))) {
|
ORDER BY `timestamp` DESC;"""))) {
|
||||||
@@ -251,10 +252,11 @@ public class MySqlDatabase extends Database {
|
|||||||
final Blob blob = resultSet.getBlob("data");
|
final Blob blob = resultSet.getBlob("data");
|
||||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||||
blob.free();
|
blob.free();
|
||||||
final VersionedUserData data = new VersionedUserData(
|
final UserDataSnapshot data = new UserDataSnapshot(
|
||||||
UUID.fromString(resultSet.getString("version_uuid")),
|
UUID.fromString(resultSet.getString("version_uuid")),
|
||||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||||
|
resultSet.getBoolean("pinned"),
|
||||||
getDataAdapter().fromBytes(dataByteArray));
|
getDataAdapter().fromBytes(dataByteArray));
|
||||||
retrievedData.add(data);
|
retrievedData.add(data);
|
||||||
}
|
}
|
||||||
@@ -268,11 +270,11 @@ public class MySqlDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompletableFuture<Optional<VersionedUserData>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
public CompletableFuture<Optional<UserDataSnapshot>> getUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
SELECT `version_uuid`, `timestamp`, `save_cause`, `data`
|
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data`
|
||||||
FROM `%user_data_table%`
|
FROM `%user_data_table%`
|
||||||
WHERE `player_uuid`=? AND `version_uuid`=?
|
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||||
ORDER BY `timestamp` DESC
|
ORDER BY `timestamp` DESC
|
||||||
@@ -284,10 +286,11 @@ public class MySqlDatabase extends Database {
|
|||||||
final Blob blob = resultSet.getBlob("data");
|
final Blob blob = resultSet.getBlob("data");
|
||||||
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
|
||||||
blob.free();
|
blob.free();
|
||||||
return Optional.of(new VersionedUserData(
|
return Optional.of(new UserDataSnapshot(
|
||||||
UUID.fromString(resultSet.getString("version_uuid")),
|
UUID.fromString(resultSet.getString("version_uuid")),
|
||||||
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
Date.from(resultSet.getTimestamp("timestamp").toInstant()),
|
||||||
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
DataSaveCause.getCauseByName(resultSet.getString("save_cause")),
|
||||||
|
resultSet.getBoolean("pinned"),
|
||||||
getDataAdapter().fromBytes(dataByteArray)));
|
getDataAdapter().fromBytes(dataByteArray)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,16 +302,19 @@ public class MySqlDatabase extends Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected CompletableFuture<Void> pruneUserData(@NotNull User user) {
|
protected CompletableFuture<Void> rotateUserData(@NotNull User user) {
|
||||||
return CompletableFuture.runAsync(() -> getUserData(user).thenAccept(data -> {
|
return CompletableFuture.runAsync(() -> {
|
||||||
if (data.size() > maxUserDataRecords) {
|
final List<UserDataSnapshot> unpinnedUserData = getUserData(user).join().stream()
|
||||||
|
.filter(dataSnapshot -> !dataSnapshot.pinned()).toList();
|
||||||
|
if (unpinnedUserData.size() > maxUserDataRecords) {
|
||||||
try (Connection connection = getConnection()) {
|
try (Connection connection = getConnection()) {
|
||||||
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
DELETE FROM `%user_data_table%`
|
DELETE FROM `%user_data_table%`
|
||||||
WHERE `player_uuid`=?
|
WHERE `player_uuid`=?
|
||||||
|
AND `pinned` IS FALSE
|
||||||
ORDER BY `timestamp` ASC
|
ORDER BY `timestamp` ASC
|
||||||
LIMIT %entry_count%;""".replace("%entry_count%",
|
LIMIT %entry_count%;""".replace("%entry_count%",
|
||||||
Integer.toString(data.size() - maxUserDataRecords))))) {
|
Integer.toString(unpinnedUserData.size() - maxUserDataRecords))))) {
|
||||||
statement.setString(1, user.uuid.toString());
|
statement.setString(1, user.uuid.toString());
|
||||||
statement.executeUpdate();
|
statement.executeUpdate();
|
||||||
}
|
}
|
||||||
@@ -316,7 +322,7 @@ public class MySqlDatabase extends Database {
|
|||||||
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
|
getLogger().log(Level.SEVERE, "Failed to prune user data from the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -361,7 +367,45 @@ public class MySqlDatabase extends Database {
|
|||||||
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
|
getLogger().log(Level.SEVERE, "Failed to set user data in the database", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).thenRun(() -> pruneUserData(user).join());
|
}).thenRun(() -> rotateUserData(user).join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> pinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
UPDATE `%user_data_table%`
|
||||||
|
SET `pinned`=TRUE
|
||||||
|
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, user.uuid.toString());
|
||||||
|
statement.setString(2, versionUuid.toString());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
getLogger().log(Level.SEVERE, "Failed to pin user data in the database", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CompletableFuture<Void> unpinUserData(@NotNull User user, @NotNull UUID versionUuid) {
|
||||||
|
return CompletableFuture.runAsync(() -> {
|
||||||
|
try (Connection connection = getConnection()) {
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
|
||||||
|
UPDATE `%user_data_table%`
|
||||||
|
SET `pinned`=FALSE
|
||||||
|
WHERE `player_uuid`=? AND `version_uuid`=?
|
||||||
|
LIMIT 1;"""))) {
|
||||||
|
statement.setString(1, user.uuid.toString());
|
||||||
|
statement.setString(2, versionUuid.toString());
|
||||||
|
statement.executeUpdate();
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
getLogger().log(Level.SEVERE, "Failed to unpin user data in the database", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import net.william278.husksync.command.Permission;
|
|||||||
import net.william278.husksync.config.Locales;
|
import net.william278.husksync.config.Locales;
|
||||||
import net.william278.husksync.data.AdvancementData;
|
import net.william278.husksync.data.AdvancementData;
|
||||||
import net.william278.husksync.data.ItemData;
|
import net.william278.husksync.data.ItemData;
|
||||||
import net.william278.husksync.data.VersionedUserData;
|
import net.william278.husksync.data.UserDataSnapshot;
|
||||||
import net.william278.husksync.player.OnlineUser;
|
import net.william278.husksync.player.OnlineUser;
|
||||||
import net.william278.husksync.player.User;
|
import net.william278.husksync.player.User;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -13,7 +13,6 @@ import java.text.DateFormat;
|
|||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides methods for displaying and editing user data
|
* Provides methods for displaying and editing user data
|
||||||
@@ -74,13 +73,13 @@ public class DataEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a chat menu detailing information about {@link VersionedUserData}
|
* Display a chat menu detailing information about {@link UserDataSnapshot}
|
||||||
*
|
*
|
||||||
* @param user The online user to display the message to
|
* @param user The online user to display the message to
|
||||||
* @param userData The {@link VersionedUserData} to display information about
|
* @param userData The {@link UserDataSnapshot} to display information about
|
||||||
* @param dataOwner The {@link User} who owns the {@link VersionedUserData}
|
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||||
*/
|
*/
|
||||||
public void displayDataOverview(@NotNull OnlineUser user, @NotNull VersionedUserData userData,
|
public void displayDataOverview(@NotNull OnlineUser user, @NotNull UserDataSnapshot userData,
|
||||||
@NotNull User dataOwner) {
|
@NotNull User dataOwner) {
|
||||||
locales.getLocale("data_manager_title",
|
locales.getLocale("data_manager_title",
|
||||||
userData.versionUUID().toString().split("-")[0],
|
userData.versionUUID().toString().split("-")[0],
|
||||||
@@ -91,6 +90,9 @@ public class DataEditor {
|
|||||||
locales.getLocale("data_manager_timestamp",
|
locales.getLocale("data_manager_timestamp",
|
||||||
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()))
|
new SimpleDateFormat("MMM dd yyyy, HH:mm:ss.sss").format(userData.versionTimestamp()))
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
|
if (userData.pinned()) {
|
||||||
|
locales.getLocale("data_manager_pinned").ifPresent(user::sendMessage);
|
||||||
|
}
|
||||||
locales.getLocale("data_manager_cause",
|
locales.getLocale("data_manager_cause",
|
||||||
userData.cause().name().toLowerCase().replaceAll("_", " "))
|
userData.cause().name().toLowerCase().replaceAll("_", " "))
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
@@ -120,7 +122,8 @@ public class DataEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private @NotNull String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData) {
|
@NotNull
|
||||||
|
private String generateAdvancementPreview(@NotNull List<AdvancementData> advancementData) {
|
||||||
final StringJoiner joiner = new StringJoiner("\n");
|
final StringJoiner joiner = new StringJoiner("\n");
|
||||||
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
final List<AdvancementData> advancementsToPreview = advancementData.stream().filter(dataItem ->
|
||||||
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
!dataItem.key.startsWith("minecraft:recipes/")).toList();
|
||||||
@@ -140,13 +143,13 @@ public class DataEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a chat list detailing a player's saved list of {@link VersionedUserData}
|
* Display a chat list detailing a player's saved list of {@link UserDataSnapshot}
|
||||||
*
|
*
|
||||||
* @param user The online user to display the message to
|
* @param user The online user to display the message to
|
||||||
* @param userDataList The list of {@link VersionedUserData} to display
|
* @param userDataList The list of {@link UserDataSnapshot} to display
|
||||||
* @param dataOwner The {@link User} who owns the {@link VersionedUserData}
|
* @param dataOwner The {@link User} who owns the {@link UserDataSnapshot}
|
||||||
*/
|
*/
|
||||||
public void displayDataList(@NotNull OnlineUser user, @NotNull List<VersionedUserData> userDataList,
|
public void displayDataList(@NotNull OnlineUser user, @NotNull List<UserDataSnapshot> userDataList,
|
||||||
@NotNull User dataOwner) {
|
@NotNull User dataOwner) {
|
||||||
locales.getLocale("data_list_title",
|
locales.getLocale("data_list_title",
|
||||||
dataOwner.username, dataOwner.uuid.toString())
|
dataOwner.username, dataOwner.uuid.toString())
|
||||||
@@ -154,7 +157,7 @@ public class DataEditor {
|
|||||||
|
|
||||||
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
|
final String[] numberedIcons = "①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳".split("");
|
||||||
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
|
for (int i = 0; i < Math.min(20, userDataList.size()); i++) {
|
||||||
final VersionedUserData userData = userDataList.get(i);
|
final UserDataSnapshot userData = userDataList.get(i);
|
||||||
locales.getLocale("data_list_item",
|
locales.getLocale("data_list_item",
|
||||||
numberedIcons[i],
|
numberedIcons[i],
|
||||||
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.getDefault())
|
||||||
@@ -162,7 +165,8 @@ public class DataEditor {
|
|||||||
userData.versionUUID().toString().split("-")[0],
|
userData.versionUUID().toString().split("-")[0],
|
||||||
userData.versionUUID().toString(),
|
userData.versionUUID().toString(),
|
||||||
userData.cause().name().toLowerCase().replaceAll("_", " "),
|
userData.cause().name().toLowerCase().replaceAll("_", " "),
|
||||||
dataOwner.username)
|
dataOwner.username,
|
||||||
|
userData.pinned() ? "※" : " ")
|
||||||
.ifPresent(user::sendMessage);
|
.ifPresent(user::sendMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import com.djrapitops.plan.extension.annotation.PluginInfo;
|
|||||||
import com.djrapitops.plan.extension.annotation.StringProvider;
|
import com.djrapitops.plan.extension.annotation.StringProvider;
|
||||||
import com.djrapitops.plan.extension.icon.Color;
|
import com.djrapitops.plan.extension.icon.Color;
|
||||||
import com.djrapitops.plan.extension.icon.Family;
|
import com.djrapitops.plan.extension.icon.Family;
|
||||||
import net.william278.husksync.data.VersionedUserData;
|
import net.william278.husksync.data.UserDataSnapshot;
|
||||||
import net.william278.husksync.database.Database;
|
import net.william278.husksync.database.Database;
|
||||||
import net.william278.husksync.player.User;
|
import net.william278.husksync.player.User;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -21,14 +21,16 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
@PluginInfo(
|
@PluginInfo(
|
||||||
name = "HuskSync",
|
name = "HuskSync",
|
||||||
iconName = "arrow-right-arrow-left",
|
iconName = "cube",
|
||||||
iconFamily = Family.SOLID,
|
iconFamily = Family.SOLID,
|
||||||
color = Color.LIGHT_BLUE
|
color = Color.NONE
|
||||||
)
|
)
|
||||||
public class PlanDataExtension implements DataExtension {
|
public class PlanDataExtension implements DataExtension {
|
||||||
|
|
||||||
private Database database;
|
private Database database;
|
||||||
|
|
||||||
|
private static final String UNKNOWN_STRING = "N/A";
|
||||||
|
|
||||||
//todo add more providers
|
//todo add more providers
|
||||||
protected PlanDataExtension(@NotNull Database database) {
|
protected PlanDataExtension(@NotNull Database database) {
|
||||||
this.database = database;
|
this.database = database;
|
||||||
@@ -45,7 +47,7 @@ public class PlanDataExtension implements DataExtension {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<Optional<VersionedUserData>> getCurrentUserData(@NotNull UUID uuid) {
|
private CompletableFuture<Optional<UserDataSnapshot>> getCurrentUserData(@NotNull UUID uuid) {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
final Optional<User> optionalUser = database.getUser(uuid).join();
|
final Optional<User> optionalUser = database.getUser(uuid).join();
|
||||||
if (optionalUser.isPresent()) {
|
if (optionalUser.isPresent()) {
|
||||||
@@ -56,11 +58,11 @@ public class PlanDataExtension implements DataExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@NumberProvider(
|
@NumberProvider(
|
||||||
text = "Data Sync Time",
|
text = "Sync Time",
|
||||||
description = "The last time the user had their data synced with the server.",
|
description = "The last time the user had their data synced with the server.",
|
||||||
iconName = "clock",
|
iconName = "clock",
|
||||||
iconFamily = Family.SOLID,
|
iconFamily = Family.SOLID,
|
||||||
format = FormatType.TIME_MILLISECONDS,
|
format = FormatType.DATE_SECOND,
|
||||||
priority = 1
|
priority = 1
|
||||||
)
|
)
|
||||||
public long getCurrentDataTimestamp(@NotNull UUID uuid) {
|
public long getCurrentDataTimestamp(@NotNull UUID uuid) {
|
||||||
@@ -70,7 +72,7 @@ public class PlanDataExtension implements DataExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@StringProvider(
|
@StringProvider(
|
||||||
text = "Data Version ID",
|
text = "Version ID",
|
||||||
description = "ID of the data version that the user is currently using.",
|
description = "ID of the data version that the user is currently using.",
|
||||||
iconName = "bolt",
|
iconName = "bolt",
|
||||||
iconFamily = Family.SOLID,
|
iconFamily = Family.SOLID,
|
||||||
@@ -80,7 +82,7 @@ public class PlanDataExtension implements DataExtension {
|
|||||||
return getCurrentUserData(uuid).join().map(
|
return getCurrentUserData(uuid).join().map(
|
||||||
versionedUserData -> versionedUserData.versionUUID().toString()
|
versionedUserData -> versionedUserData.versionUUID().toString()
|
||||||
.split(Pattern.quote("-"))[0])
|
.split(Pattern.quote("-"))[0])
|
||||||
.orElse("unknown");
|
.orElse(UNKNOWN_STRING);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NumberProvider(
|
@NumberProvider(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import net.william278.husksync.HuskSync;
|
|||||||
import net.william278.husksync.config.Settings;
|
import net.william278.husksync.config.Settings;
|
||||||
import net.william278.husksync.data.ItemData;
|
import net.william278.husksync.data.ItemData;
|
||||||
import net.william278.husksync.data.DataSaveCause;
|
import net.william278.husksync.data.DataSaveCause;
|
||||||
import net.william278.husksync.data.UserData;
|
|
||||||
import net.william278.husksync.player.OnlineUser;
|
import net.william278.husksync.player.OnlineUser;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
@@ -23,7 +22,7 @@ public abstract class EventListener {
|
|||||||
/**
|
/**
|
||||||
* The plugin instance
|
* The plugin instance
|
||||||
*/
|
*/
|
||||||
protected final HuskSync huskSync;
|
protected final HuskSync plugin;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set of UUIDs current awaiting item synchronization. Events will be cancelled for these users
|
* Set of UUIDs current awaiting item synchronization. Events will be cancelled for these users
|
||||||
@@ -35,8 +34,8 @@ public abstract class EventListener {
|
|||||||
*/
|
*/
|
||||||
private boolean disabling;
|
private boolean disabling;
|
||||||
|
|
||||||
protected EventListener(@NotNull HuskSync huskSync) {
|
protected EventListener(@NotNull HuskSync plugin) {
|
||||||
this.huskSync = huskSync;
|
this.plugin = plugin;
|
||||||
this.usersAwaitingSync = new HashSet<>();
|
this.usersAwaitingSync = new HashSet<>();
|
||||||
this.disabling = false;
|
this.disabling = false;
|
||||||
}
|
}
|
||||||
@@ -49,18 +48,15 @@ public abstract class EventListener {
|
|||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
try {
|
try {
|
||||||
// Hold reading data for the network latency threshold, to ensure the source server has set the redis key
|
// Hold reading data for the network latency threshold, to ensure the source server has set the redis key
|
||||||
Thread.sleep(Math.min(0, huskSync.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS)));
|
Thread.sleep(Math.max(0, plugin.getSettings().getIntegerValue(Settings.ConfigOption.SYNCHRONIZATION_NETWORK_LATENCY_MILLISECONDS)));
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
huskSync.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e);
|
plugin.getLoggingAdapter().log(Level.SEVERE, "An exception occurred handling a player join", e);
|
||||||
} finally {
|
} finally {
|
||||||
huskSync.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
|
plugin.getRedisManager().getUserServerSwitch(user).thenAccept(changingServers -> {
|
||||||
huskSync.getLoggingAdapter().info("Handling server change check " + ((changingServers) ? "true" : "false"));
|
|
||||||
if (!changingServers) {
|
if (!changingServers) {
|
||||||
huskSync.getLoggingAdapter().info("User is not changing servers");
|
|
||||||
// Fetch from the database if the user isn't changing servers
|
// Fetch from the database if the user isn't changing servers
|
||||||
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
|
setUserFromDatabase(user).thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded));
|
||||||
} else {
|
} else {
|
||||||
huskSync.getLoggingAdapter().info("User is changing servers, setting from db");
|
|
||||||
final int TIME_OUT_MILLISECONDS = 3200;
|
final int TIME_OUT_MILLISECONDS = 3200;
|
||||||
CompletableFuture.runAsync(() -> {
|
CompletableFuture.runAsync(() -> {
|
||||||
final AtomicInteger currentMilliseconds = new AtomicInteger(0);
|
final AtomicInteger currentMilliseconds = new AtomicInteger(0);
|
||||||
@@ -70,20 +66,19 @@ public abstract class EventListener {
|
|||||||
executor.scheduleAtFixedRate(() -> {
|
executor.scheduleAtFixedRate(() -> {
|
||||||
if (user.isOffline()) {
|
if (user.isOffline()) {
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
huskSync.getLoggingAdapter().info("Cancelled sync, user gone offline!");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
|
if (disabling || currentMilliseconds.get() > TIME_OUT_MILLISECONDS) {
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
setUserFromDatabase(user).thenRun(() -> handleSynchronisationCompletion(user));
|
setUserFromDatabase(user)
|
||||||
huskSync.getLoggingAdapter().info("Setting user from db as fallback");
|
.thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
huskSync.getRedisManager().getUserData(user).thenAccept(redisUserData ->
|
plugin.getRedisManager().getUserData(user).thenAccept(redisUserData ->
|
||||||
redisUserData.ifPresent(redisData -> {
|
redisUserData.ifPresent(redisData -> {
|
||||||
huskSync.getLoggingAdapter().info("Setting user from redis!");
|
user.setData(redisData, plugin.getSettings(), plugin.getEventCannon(),
|
||||||
user.setData(redisData, huskSync.getSettings(), huskSync.getEventCannon())
|
plugin.getLoggingAdapter(), plugin.getMinecraftVersion())
|
||||||
.thenRun(() -> handleSynchronisationCompletion(user)).join();
|
.thenAccept(succeeded -> handleSynchronisationCompletion(user, succeeded)).join();
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
})).join();
|
})).join();
|
||||||
currentMilliseconds.addAndGet(200);
|
currentMilliseconds.addAndGet(200);
|
||||||
@@ -95,18 +90,39 @@ public abstract class EventListener {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private CompletableFuture<Void> setUserFromDatabase(@NotNull OnlineUser user) {
|
/**
|
||||||
return huskSync.getDatabase().getCurrentUserData(user)
|
* Set a user's data from the database
|
||||||
.thenAccept(databaseUserData -> databaseUserData.ifPresent(databaseData ->
|
*
|
||||||
user.setData(databaseData.userData(), huskSync.getSettings(),
|
* @param user The user to set the data for
|
||||||
huskSync.getEventCannon()).join()));
|
* @return Whether the data was successfully set
|
||||||
|
*/
|
||||||
|
private CompletableFuture<Boolean> setUserFromDatabase(@NotNull OnlineUser user) {
|
||||||
|
return plugin.getDatabase().getCurrentUserData(user).thenApply(databaseUserData -> {
|
||||||
|
if (databaseUserData.isPresent()) {
|
||||||
|
return user.setData(databaseUserData.get().userData(), plugin.getSettings(), plugin.getEventCannon(),
|
||||||
|
plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).join();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleSynchronisationCompletion(@NotNull OnlineUser user) {
|
/**
|
||||||
huskSync.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
|
* Handle a player's synchronization completion
|
||||||
usersAwaitingSync.remove(user.uuid);
|
*
|
||||||
huskSync.getDatabase().ensureUser(user).join();
|
* @param user The {@link OnlineUser} to handle
|
||||||
huskSync.getEventCannon().fireSyncCompleteEvent(user);
|
* @param succeeded Whether the synchronization succeeded
|
||||||
|
*/
|
||||||
|
private void handleSynchronisationCompletion(@NotNull OnlineUser user, boolean succeeded) {
|
||||||
|
if (succeeded) {
|
||||||
|
plugin.getLocales().getLocale("synchronisation_complete").ifPresent(user::sendActionBar);
|
||||||
|
usersAwaitingSync.remove(user.uuid);
|
||||||
|
plugin.getDatabase().ensureUser(user).join();
|
||||||
|
plugin.getEventCannon().fireSyncCompleteEvent(user);
|
||||||
|
} else {
|
||||||
|
plugin.getLocales().getLocale("synchronisation_failed")
|
||||||
|
.ifPresent(user::sendMessage);
|
||||||
|
plugin.getDatabase().ensureUser(user).join();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void handlePlayerQuit(@NotNull OnlineUser user) {
|
public final void handlePlayerQuit(@NotNull OnlineUser user) {
|
||||||
@@ -118,42 +134,42 @@ public abstract class EventListener {
|
|||||||
if (usersAwaitingSync.contains(user.uuid)) {
|
if (usersAwaitingSync.contains(user.uuid)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
huskSync.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept(
|
plugin.getRedisManager().setUserServerSwitch(user).thenRun(() -> user.getUserData().thenAccept(
|
||||||
userData -> huskSync.getRedisManager().setUserData(user, userData).thenRun(
|
userData -> plugin.getRedisManager().setUserData(user, userData).thenRun(
|
||||||
() -> huskSync.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join())));
|
() -> plugin.getDatabase().setUserData(user, userData, DataSaveCause.DISCONNECT).join())));
|
||||||
usersAwaitingSync.remove(user.uuid);
|
usersAwaitingSync.remove(user.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {
|
public final void handleWorldSave(@NotNull List<OnlineUser> usersInWorld) {
|
||||||
if (disabling || !huskSync.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) {
|
if (disabling || !plugin.getSettings().getBooleanValue(Settings.ConfigOption.SYNCHRONIZATION_SAVE_ON_WORLD_SAVE)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->
|
CompletableFuture.runAsync(() -> usersInWorld.forEach(user ->
|
||||||
huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.WORLD_SAVE).join()));
|
plugin.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.WORLD_SAVE).join()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void handlePluginDisable() {
|
public final void handlePluginDisable() {
|
||||||
disabling = true;
|
disabling = true;
|
||||||
|
|
||||||
huskSync.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user ->
|
plugin.getOnlineUsers().stream().filter(user -> !usersAwaitingSync.contains(user.uuid)).forEach(user ->
|
||||||
huskSync.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join());
|
plugin.getDatabase().setUserData(user, user.getUserData().join(), DataSaveCause.SERVER_SHUTDOWN).join());
|
||||||
|
|
||||||
huskSync.getDatabase().close();
|
plugin.getDatabase().close();
|
||||||
huskSync.getRedisManager().close();
|
plugin.getRedisManager().close();
|
||||||
}
|
}
|
||||||
|
|
||||||
public final void handleMenuClose(@NotNull OnlineUser user, @NotNull ItemData menuInventory) {
|
public final void handleMenuClose(@NotNull OnlineUser user, @NotNull ItemData menuInventory) {
|
||||||
if (disabling) {
|
if (disabling) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
huskSync.getDataEditor().closeInventoryMenu(user, menuInventory);
|
plugin.getDataEditor().closeInventoryMenu(user, menuInventory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean cancelMenuClick(@NotNull OnlineUser user) {
|
public final boolean cancelMenuClick(@NotNull OnlineUser user) {
|
||||||
if (disabling) {
|
if (disabling) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return huskSync.getDataEditor().cancelInventoryEdit(user);
|
return plugin.getDataEditor().cancelInventoryEdit(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public final boolean cancelPlayerEvent(@NotNull OnlineUser user) {
|
public final boolean cancelPlayerEvent(@NotNull OnlineUser user) {
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ import net.william278.husksync.data.*;
|
|||||||
import net.william278.husksync.editor.ItemEditorMenu;
|
import net.william278.husksync.editor.ItemEditorMenu;
|
||||||
import net.william278.husksync.event.EventCannon;
|
import net.william278.husksync.event.EventCannon;
|
||||||
import net.william278.husksync.event.PreSyncEvent;
|
import net.william278.husksync.event.PreSyncEvent;
|
||||||
|
import net.william278.husksync.util.Logger;
|
||||||
|
import net.william278.husksync.util.Version;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a logged-in {@link User}
|
* Represents a logged-in {@link User}
|
||||||
@@ -159,16 +163,32 @@ public abstract class OnlineUser extends User {
|
|||||||
*/
|
*/
|
||||||
public abstract boolean isOffline();
|
public abstract boolean isOffline();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the implementing Minecraft server version
|
||||||
|
*
|
||||||
|
* @return The Minecraft server version
|
||||||
|
*/
|
||||||
|
@NotNull
|
||||||
|
public abstract Version getMinecraftVersion();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set {@link UserData} to a player
|
* Set {@link UserData} to a player
|
||||||
*
|
*
|
||||||
* @param data The data to set
|
* @param data The data to set
|
||||||
* @param settings Plugin settings, for determining what needs setting
|
* @param settings Plugin settings, for determining what needs setting
|
||||||
* @return a future that will be completed when done
|
* @return a future returning a boolean when complete; if the sync was successful, the future will return {@code true}
|
||||||
*/
|
*/
|
||||||
public final CompletableFuture<Void> setData(@NotNull UserData data, @NotNull Settings settings,
|
public final CompletableFuture<Boolean> setData(@NotNull UserData data, @NotNull Settings settings,
|
||||||
@NotNull EventCannon eventCannon) {
|
@NotNull EventCannon eventCannon, @NotNull Logger logger,
|
||||||
return CompletableFuture.runAsync(() -> {
|
@NotNull Version serverMinecraftVersion) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
// Prevent synchronizing 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 + "\"");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join();
|
final PreSyncEvent preSyncEvent = (PreSyncEvent) eventCannon.firePreSyncEvent(this, data).join();
|
||||||
final UserData finalData = preSyncEvent.getUserData();
|
final UserData finalData = preSyncEvent.getUserData();
|
||||||
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
|
final List<CompletableFuture<Void>> dataSetOperations = new ArrayList<>() {{
|
||||||
@@ -197,7 +217,14 @@ public abstract class OnlineUser extends User {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).join();
|
// Apply operations in parallel, join when complete
|
||||||
|
return CompletableFuture.allOf(dataSetOperations.toArray(new CompletableFuture[0])).thenApply(unused -> true)
|
||||||
|
.exceptionally(exception -> {
|
||||||
|
// Handle synchronisation exceptions
|
||||||
|
logger.log(Level.SEVERE, "Failed to set data for player " + username + " (" + exception.getMessage() + ")");
|
||||||
|
exception.printStackTrace();
|
||||||
|
return false;
|
||||||
|
}).join();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -240,7 +267,8 @@ public abstract class OnlineUser extends User {
|
|||||||
return CompletableFuture.supplyAsync(
|
return CompletableFuture.supplyAsync(
|
||||||
() -> new UserData(getStatus().join(), getInventory().join(),
|
() -> new UserData(getStatus().join(), 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().getWithoutMeta()));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,10 +83,16 @@ public class RedisManager {
|
|||||||
final RedisMessage redisMessage = RedisMessage.fromJson(message);
|
final RedisMessage redisMessage = RedisMessage.fromJson(message);
|
||||||
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
|
plugin.getOnlineUser(redisMessage.targetUserUuid).ifPresent(user -> {
|
||||||
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
|
final UserData userData = plugin.getDataAdapter().fromBytes(redisMessage.data);
|
||||||
user.setData(userData, plugin.getSettings(), plugin.getEventCannon()).thenRun(() -> {
|
user.setData(userData, plugin.getSettings(), plugin.getEventCannon(),
|
||||||
plugin.getLocales().getLocale("data_update_complete")
|
plugin.getLoggingAdapter(), plugin.getMinecraftVersion()).thenAccept(succeeded -> {
|
||||||
.ifPresent(user::sendActionBar);
|
if (succeeded) {
|
||||||
plugin.getEventCannon().fireSyncCompleteEvent(user);
|
plugin.getLocales().getLocale("data_update_complete")
|
||||||
|
.ifPresent(user::sendActionBar);
|
||||||
|
plugin.getEventCannon().fireSyncCompleteEvent(user);
|
||||||
|
} else {
|
||||||
|
plugin.getLocales().getLocale("data_update_failed")
|
||||||
|
.ifPresent(user::sendMessage);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,31 +13,31 @@ public class UpdateChecker {
|
|||||||
|
|
||||||
private final static int SPIGOT_PROJECT_ID = 97144;
|
private final static int SPIGOT_PROJECT_ID = 97144;
|
||||||
private final Logger logger;
|
private final Logger logger;
|
||||||
private final VersionUtils.Version currentVersion;
|
private final Version currentVersion;
|
||||||
|
|
||||||
public UpdateChecker(@NotNull String currentVersion, @NotNull Logger logger) {
|
public UpdateChecker(@NotNull Version currentVersion, @NotNull Logger logger) {
|
||||||
this.currentVersion = VersionUtils.Version.of(currentVersion);
|
this.currentVersion = currentVersion;
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<VersionUtils.Version> fetchLatestVersion() {
|
public CompletableFuture<Version> fetchLatestVersion() {
|
||||||
return CompletableFuture.supplyAsync(() -> {
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
try {
|
try {
|
||||||
final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
|
final URL url = new URL("https://api.spigotmc.org/legacy/update.php?resource=" + SPIGOT_PROJECT_ID);
|
||||||
URLConnection urlConnection = url.openConnection();
|
URLConnection urlConnection = url.openConnection();
|
||||||
return VersionUtils.Version.of(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
|
return Version.pluginVersion(new BufferedReader(new InputStreamReader(urlConnection.getInputStream())).readLine());
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.log(Level.WARNING, "Failed to fetch the latest plugin version", e);
|
logger.log(Level.WARNING, "Failed to fetch the latest plugin version", e);
|
||||||
}
|
}
|
||||||
return new VersionUtils.Version();
|
return new Version();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isUpdateAvailable(@NotNull VersionUtils.Version latestVersion) {
|
public boolean isUpdateAvailable(@NotNull Version latestVersion) {
|
||||||
return latestVersion.compareTo(currentVersion) > 0;
|
return latestVersion.compareTo(currentVersion) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VersionUtils.Version getCurrentVersion() {
|
public Version getCurrentVersion() {
|
||||||
return currentVersion;
|
return currentVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package net.william278.husksync.util;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class Version implements Comparable<Version> {
|
||||||
|
private final static String VERSION_SEPARATOR = ".";
|
||||||
|
private final static String MINECRAFT_META_SEPARATOR = "-";
|
||||||
|
private final static String PLUGIN_META_SEPARATOR = "+";
|
||||||
|
|
||||||
|
private int[] versions = new int[]{};
|
||||||
|
@NotNull
|
||||||
|
private String metadata = "";
|
||||||
|
@NotNull
|
||||||
|
private String metaSeparator = "";
|
||||||
|
|
||||||
|
protected Version() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private Version(@NotNull String version, @NotNull String metaSeparator) {
|
||||||
|
this.parse(version, metaSeparator);
|
||||||
|
this.metaSeparator = metaSeparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static Version pluginVersion(@NotNull String versionString) {
|
||||||
|
return new Version(versionString, PLUGIN_META_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public static Version minecraftVersion(@NotNull String versionString) {
|
||||||
|
return new Version(versionString, MINECRAFT_META_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parse(@NotNull String version, @NotNull String metaSeparator) {
|
||||||
|
int metaIndex = version.indexOf(metaSeparator);
|
||||||
|
if (metaIndex > 0) {
|
||||||
|
this.metadata = version.substring(metaIndex + 1);
|
||||||
|
version = version.substring(0, metaIndex);
|
||||||
|
}
|
||||||
|
String[] versions = version.split(Pattern.quote(VERSION_SEPARATOR));
|
||||||
|
this.versions = Arrays.stream(versions).mapToInt(Integer::parseInt).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int compareTo(@NotNull Version other) {
|
||||||
|
int length = Math.max(this.versions.length, other.versions.length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
int a = i < this.versions.length ? this.versions[i] : 0;
|
||||||
|
int b = i < other.versions.length ? other.versions[i] : 0;
|
||||||
|
|
||||||
|
if (a < b) return -1;
|
||||||
|
if (a > b) return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
final StringJoiner joiner = new StringJoiner(VERSION_SEPARATOR);
|
||||||
|
for (int version : this.versions) {
|
||||||
|
joiner.add(String.valueOf(version));
|
||||||
|
}
|
||||||
|
return joiner + this.metaSeparator + this.metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String getWithoutMeta() {
|
||||||
|
final StringJoiner joiner = new StringJoiner(VERSION_SEPARATOR);
|
||||||
|
for (int version : this.versions) {
|
||||||
|
joiner.add(String.valueOf(version));
|
||||||
|
}
|
||||||
|
return joiner.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
public String getMetadata() {
|
||||||
|
return this.metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
package net.william278.husksync.util;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
public class VersionUtils {
|
|
||||||
|
|
||||||
private final static char META_SEPARATOR = '+';
|
|
||||||
private final static String VERSION_SEPARATOR = "\\.";
|
|
||||||
|
|
||||||
|
|
||||||
public static class Version implements Comparable<Version> {
|
|
||||||
public int[] versions = new int[]{};
|
|
||||||
public String metadata = "";
|
|
||||||
|
|
||||||
public Version() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Version(String version) {
|
|
||||||
this.parse(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Version of(String version) {
|
|
||||||
return new Version(version);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void parse(String version) {
|
|
||||||
int metaIndex = version.indexOf(META_SEPARATOR);
|
|
||||||
if (metaIndex > 0) {
|
|
||||||
this.metadata = version.substring(metaIndex + 1);
|
|
||||||
version = version.substring(0, metaIndex);
|
|
||||||
}
|
|
||||||
String[] versions = version.split(VERSION_SEPARATOR);
|
|
||||||
this.versions = Arrays.stream(versions).mapToInt(Integer::parseInt).toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int compareTo(Version version) {
|
|
||||||
int length = Math.max(this.versions.length, version.versions.length);
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
int a = i < this.versions.length ? this.versions[i] : 0;
|
|
||||||
int b = i < version.versions.length ? version.versions[i] : 0;
|
|
||||||
|
|
||||||
if (a < b) return -1;
|
|
||||||
if (a > b) return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
StringBuilder stringBuffer = new StringBuilder();
|
|
||||||
for (int version : this.versions) {
|
|
||||||
stringBuffer.append(version).append('.');
|
|
||||||
}
|
|
||||||
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
|
|
||||||
return stringBuffer.append('+').append(this.metadata).toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
language: 'en-gb'
|
language: 'en-gb'
|
||||||
check_for_updates: true
|
check_for_updates: true
|
||||||
cluster_id: ''
|
cluster_id: ''
|
||||||
debug_logging: true
|
debug_logging: false
|
||||||
|
|
||||||
database:
|
database:
|
||||||
credentials:
|
credentials:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS `%users_table%`
|
|||||||
CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
CREATE TABLE IF NOT EXISTS `%user_data_table%`
|
||||||
(
|
(
|
||||||
`version_uuid` char(36) NOT NULL UNIQUE,
|
`version_uuid` char(36) NOT NULL UNIQUE,
|
||||||
`player_uuid` char(36) NOT NULL UNIQUE,
|
`player_uuid` char(36) NOT NULL,
|
||||||
`timestamp` datetime NOT NULL,
|
`timestamp` datetime NOT NULL,
|
||||||
`save_cause` varchar(32) NOT NULL,
|
`save_cause` varchar(32) NOT NULL,
|
||||||
`pinned` boolean NOT NULL DEFAULT FALSE,
|
`pinned` boolean NOT NULL DEFAULT FALSE,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
synchronisation_complete: '[⏵ Data synchronised!](#00fb9a)'
|
synchronisation_complete: '[⏵ Data synchronised!](#00fb9a)'
|
||||||
|
synchronisation_failed: '[⏵ Failed to synchronise your data! Please contact an administrator.](#ff7e5e)'
|
||||||
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)'
|
reload_complete: '[HuskSync](#00fb9a bold) [| Reloaded config and message files.](#00fb9a)'
|
||||||
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)'
|
error_invalid_syntax: '[Error:](#ff3300) [Incorrect syntax. Usage: %1%](#ff7e5e)'
|
||||||
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
error_invalid_player: '[Error:](#ff3300) [Could not find a player by that name.](#ff7e5e)'
|
||||||
@@ -12,15 +13,19 @@ 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)'
|
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)'
|
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_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_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&7When the data was saved)'
|
data_manager_timestamp: '[⌚ %1%](#ffc43b-#f5c962 show_text=&7Version timestamp:\\n&8When the data was saved)'
|
||||||
data_manager_cause: '[⚑ %1%](#23a825-#36f539 show_text=&7Save cause:\\n&7What caused the data to be saved)\\n'
|
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_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=&7%2%) [⌛ Play Time: %3%ʜʀs](color=#62a9f5-#7ab8fa show_text=&7⚠ Based on in-game statistics)'
|
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_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.\\nThis 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.\\nThis will set the user''s data to this snapshot.\\n&#ff3300&⚠ %1%''s current data will be overwritten! run_command=/userdata restore %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_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_title: '[List of](#00fb9a) [%1%](#00fb9a bold show_text=&7UUID: %2%)[''s user data snapshots:](#00fb9a)\\n'
|
||||||
data_list_item: '[%1%](gray show_text=&7Snapshot %3% 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_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&7%2%) [for](#00fb9a) [%3%.](#00fb9a show_text=&7Player UUID:\\n&7%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&7%2%)[''s current user data from snapshot](#00fb9a) [%3%.](#00fb9a show_text=&7Version UUID:\\n&7%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%)'
|
||||||
Reference in New Issue
Block a user