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

Compare commits

..

10 Commits
3.3.1 ... 3.3.3

Author SHA1 Message Date
William278
67ef4888da fix: death save updating player 2024-02-17 14:55:19 +00:00
William278
a5d3015c6e feat: allow customizable save / update causes 2024-02-13 16:23:33 +00:00
William278
131a364f53 fix: cache not cleared on /userdata delete, close #245 2024-02-13 14:38:19 +00:00
William
19636d9447 refactor: optimize imports 2024-02-12 17:51:54 +00:00
William
f803a0b57b refactor: revert keys change 2024-02-12 17:51:40 +00:00
William
28afffe95e refactor/redis: use scan instead of keys 2024-02-12 17:19:05 +00:00
dependabot[bot]
c7e100a78a deps: bump org.json:json from 20231013 to 20240205 (#244)
Bumps [org.json:json](https://github.com/douglascrockford/JSON-java) from 20231013 to 20240205.
- [Release notes](https://github.com/douglascrockford/JSON-java/releases)
- [Changelog](https://github.com/stleary/JSON-java/blob/master/docs/RELEASES.md)
- [Commits](https://github.com/douglascrockford/JSON-java/commits)

---
updated-dependencies:
- dependency-name: org.json:json
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 10:02:57 +00:00
William
12e223618d refactor: data save event order processing, use new method in DataSyncer (#243)
* fix: fire DataSaveEvent before disconnect

* fix: revert rename `addSnapshot`

* docs: mention `addSnapshot` firing the API event

* refactor: use DataSyncer method for event saving, close #242

* fix: trailing semicolon
2024-02-11 15:37:03 +00:00
William278
f6773f4e68 build: bump to 3.3.2 2024-02-11 14:27:04 +00:00
William278
b9434a56e8 refactor: minor Bukkit platform refactors 2024-02-11 14:26:48 +00:00
17 changed files with 345 additions and 140 deletions

View File

@@ -15,7 +15,7 @@ dependencies {
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'org.projectlombok:lombok:1.18.30'
compileOnly 'commons-io:commons-io:2.15.1'
compileOnly 'org.json:json:20231013'
compileOnly 'org.json:json:20240205'
compileOnly 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
compileOnly 'com.zaxxer:HikariCP:5.1.0'

View File

@@ -60,7 +60,6 @@ import net.william278.husksync.util.BukkitMapPersister;
import net.william278.husksync.util.BukkitTask;
import net.william278.husksync.util.LegacyConverter;
import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.map.MapView;
import org.bukkit.plugin.java.JavaPlugin;
@@ -229,7 +228,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
@NotNull
public Set<OnlineUser> getOnlineUsers() {
return Bukkit.getOnlinePlayers().stream()
return getServer().getOnlinePlayers().stream()
.map(player -> BukkitUser.adapt(player, this))
.collect(Collectors.toSet());
}
@@ -237,7 +236,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
@NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
final Player player = Bukkit.getPlayer(uuid);
final Player player = getServer().getPlayer(uuid);
if (player == null) {
return Optional.empty();
}
@@ -253,12 +252,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@NotNull
@Override
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
if (playerCustomDataStore.containsKey(user.getUuid())) {
return playerCustomDataStore.get(user.getUuid());
}
final Map<Identifier, Data> data = Maps.newHashMap();
playerCustomDataStore.put(user.getUuid(), data);
return data;
return playerCustomDataStore.compute(
user.getUuid(),
(uuid, data) -> data == null ? Maps.newHashMap() : data
);
}
@Override
@@ -269,7 +266,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
public boolean isDependencyLoaded(@NotNull String name) {
return Bukkit.getPluginManager().getPlugin(name) != null;
return getServer().getPluginManager().getPlugin(name) != null;
}
// Register bStats metrics
@@ -303,7 +300,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@NotNull
@Override
public Version getMinecraftVersion() {
return Version.fromString(Bukkit.getBukkitVersion());
return Version.fromString(getServer().getBukkitVersion());
}
@NotNull
@@ -347,7 +344,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override
@NotNull
public HuskSync getPlugin() {
public BukkitHuskSync getPlugin() {
return this;
}

View File

@@ -25,7 +25,6 @@ import org.bukkit.entity.Player;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Optional;
public interface BukkitUserDataHolder extends UserDataHolder {
@@ -140,9 +139,6 @@ public interface BukkitUserDataHolder extends UserDataHolder {
@NotNull
Player getBukkitPlayer();
@NotNull
Map<Identifier, Data> getCustomDataStore();
@NotNull
default BukkitMapPersister getMapPersister() {
return (BukkitHuskSync) getPlugin();

View File

@@ -25,7 +25,7 @@ import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
import net.querz.nbt.io.NBTUtil;
import net.querz.nbt.tag.CompoundTag;
import net.william278.husksync.HuskSync;
import net.william278.husksync.BukkitHuskSync;
import net.william278.mapdataapi.MapBanner;
import net.william278.mapdataapi.MapData;
import org.bukkit.Bukkit;
@@ -85,7 +85,7 @@ public interface BukkitMapPersister {
// Perform an operation on each map in an array of ItemStacks
@NotNull
private ItemStack[] forEachMap(@NotNull ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
private ItemStack[] forEachMap(ItemStack[] items, @NotNull Function<ItemStack, ItemStack> function) {
for (int i = 0; i < items.length; i++) {
final ItemStack item = items[i];
if (item == null) {
@@ -148,7 +148,7 @@ public interface BukkitMapPersister {
// Search for an existing map view
Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) {
world = Bukkit.getWorlds().stream()
world = getPlugin().getServer().getWorlds().stream()
.map(w -> w.getUID().toString()).filter(u -> u.equals(worldUid))
.findFirst();
if (world.isPresent()) {
@@ -441,6 +441,6 @@ public interface BukkitMapPersister {
@ApiStatus.Internal
@NotNull
HuskSync getPlugin();
BukkitHuskSync getPlugin();
}

View File

@@ -6,7 +6,7 @@ dependencies {
api 'commons-io:commons-io:2.15.1'
api 'org.apache.commons:commons-text:1.11.0'
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
api 'org.json:json:20231013'
api 'org.json:json:20240205'
api 'com.google.code.gson:gson:2.10.1'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'

View File

@@ -31,11 +31,13 @@ import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/**
* The common implementation of the HuskSync API, containing cross-platform API calls.
@@ -262,13 +264,32 @@ public class HuskSyncAPI {
*
* @param user The user to save the data for
* @param snapshot The snapshot to save
* @param callback A callback to run after the data has been saved (if the DataSaveEvent was not cancelled)
* @apiNote This will fire the {@link net.william278.husksync.event.DataSaveEvent} event, unless
* the save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}
* @since 3.3.2
*/
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot,
@Nullable BiConsumer<User, DataSnapshot.Packed> callback) {
plugin.runAsync(() -> plugin.getDataSyncer().saveData(
user,
snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot,
callback
));
}
/**
* Adds a data snapshot to the database
*
* @param user The user to save the data for
* @param snapshot The snapshot to save
* @apiNote This will fire the {@link net.william278.husksync.event.DataSaveEvent} event, unless
* * the save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}
* @since 3.0
*/
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
plugin.runAsync(() -> plugin.getDatabase().addSnapshot(
user, snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
));
this.addSnapshot(user, snapshot, null);
}
/**

View File

@@ -72,8 +72,8 @@ public class EnderChestCommand extends ItemsCommand {
// Creates a new snapshot with the updated enderChest
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
@@ -90,10 +90,12 @@ public class EnderChestCommand extends ItemsCommand {
);
});
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDatabase().addSnapshot(user, snapshot);
redis.sendUserDataUpdate(user, snapshot);
redis.getUserData(user).ifPresent(data -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
redis.sendUserDataUpdate(user, data);
});
}
}

View File

@@ -72,8 +72,8 @@ public class InventoryCommand extends ItemsCommand {
// Creates a new snapshot with the updated inventory
@SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user);
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage);
@@ -90,10 +90,12 @@ public class InventoryCommand extends ItemsCommand {
);
});
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDatabase().addSnapshot(user, snapshot);
redis.sendUserDataUpdate(user, snapshot);
redis.getUserData(user).ifPresent(data -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
plugin.getDataSyncer().saveData(holder, snapshot, (user, data) -> {
redis.getUserData(user).ifPresent(d -> redis.setUserData(user, snapshot, RedisKeyType.TTL_1_YEAR));
redis.sendUserDataUpdate(user, data);
});
}
}

View File

@@ -21,6 +21,8 @@ package net.william278.husksync.command;
import net.william278.husksync.HuskSync;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.CommandUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.DataDumper;
@@ -110,13 +112,14 @@ public class UserDataCommand extends Command implements TabProvider {
return;
}
// Delete user data by specified UUID
// Delete user data by specified UUID and clear their data cache
final UUID version = optionalUuid.get();
if (!plugin.getDatabase().deleteSnapshot(user, version)) {
plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage);
return;
}
plugin.getRedisManager().clearUserData(user);
plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0],
@@ -152,11 +155,14 @@ public class UserDataCommand extends Command implements TabProvider {
);
}));
// Set the user's data and send a message
plugin.getDatabase().addSnapshot(user, data);
plugin.getRedisManager().sendUserDataUpdate(user, data);
plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(),
data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage);
// Save data
final RedisManager redis = plugin.getRedisManager();
plugin.getDataSyncer().saveData(user, data, (u, s) -> {
redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
redis.sendUserDataUpdate(u, s);
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
});
}
case "pin" -> {

View File

@@ -23,10 +23,16 @@ import com.google.common.collect.Maps;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
import de.themoep.minedown.adventure.MineDown;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import net.william278.desertwell.util.Version;
import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.adapter.DataAdapter;
import org.apache.commons.text.WordUtils;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -41,6 +47,7 @@ import java.util.stream.Collectors;
*
* @since 3.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DataSnapshot {
/*
@@ -59,7 +66,7 @@ public class DataSnapshot {
protected OffsetDateTime timestamp;
@SerializedName("save_cause")
protected SaveCause saveCause;
protected String saveCause;
@SerializedName("server_name")
protected String serverName;
@@ -77,7 +84,7 @@ public class DataSnapshot {
protected Map<String, String> data;
private DataSnapshot(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
this.id = id;
this.pinned = pinned;
@@ -90,10 +97,6 @@ public class DataSnapshot {
this.formatVersion = formatVersion;
}
@SuppressWarnings("unused")
private DataSnapshot() {
}
@NotNull
@ApiStatus.Internal
public static DataSnapshot.Builder builder(@NotNull HuskSync plugin) {
@@ -196,7 +199,7 @@ public class DataSnapshot {
*/
@NotNull
public SaveCause getSaveCause() {
return saveCause;
return SaveCause.of(saveCause);
}
/**
@@ -219,7 +222,7 @@ public class DataSnapshot {
* @since 3.0
*/
public void setSaveCause(@NotNull SaveCause saveCause) {
this.saveCause = saveCause;
this.saveCause = saveCause.name();
}
/**
@@ -270,24 +273,21 @@ public class DataSnapshot {
*
* @since 3.0
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Packed extends DataSnapshot implements Adaptable {
protected Packed(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
}
@SuppressWarnings("unused")
private Packed() {
}
@ApiStatus.Internal
public void edit(@NotNull HuskSync plugin, @NotNull Consumer<Unpacked> editor) {
final Unpacked data = unpack(plugin);
editor.accept(data);
this.pinned = data.isPinned();
this.saveCause = data.getSaveCause();
this.saveCause = data.getSaveCause().name();
this.data = data.serializeData(plugin);
}
@@ -341,7 +341,7 @@ public class DataSnapshot {
private final Map<Identifier, Data> deserialized;
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion,
@NotNull HuskSync plugin) {
super(id, pinned, timestamp, saveCause, serverName, data, minecraftVersion, platformType, formatVersion);
@@ -349,7 +349,7 @@ public class DataSnapshot {
}
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull SaveCause saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
this.deserialized = data;
@@ -719,7 +719,7 @@ public class DataSnapshot {
id,
pinned || plugin.getSettings().getSynchronization().doAutoPin(saveCause),
timestamp,
saveCause,
saveCause.name(),
serverName,
data,
plugin.getMinecraftVersion(),
@@ -742,138 +742,253 @@ public class DataSnapshot {
}
public interface Cause {
@NotNull
String name();
/**
* Returns the capitalized display name of the cause.
*
* @return the cause display name
*/
@NotNull
default String getDisplayName() {
return WordUtils.capitalizeFully(name().replaceAll("_", " "));
}
}
/**
* Identifies the cause of a player data save.
*
* @implNote This enum is saved in the database.
* A string wrapper, for identifying the cause of a player data save.
* </p>
* Cause names have a max length of 32 characters.
*/
public enum SaveCause {
@Accessors(fluent = true)
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class SaveCause implements Cause {
/**
* Indicates data saved when a player disconnected from the server (either to change servers, or to log off)
*
* @since 2.0
*/
DISCONNECT,
public static final SaveCause DISCONNECT = of("DISCONNECT");
/**
* Indicates data saved when the world saved
*
* @since 2.0
*/
WORLD_SAVE,
public static final SaveCause WORLD_SAVE = of("WORLD_SAVE");
/**
* Indicates data saved when the user died
*
* @since 2.1
*/
DEATH,
public static final SaveCause DEATH = of("DEATH");
/**
* Indicates data saved when the server shut down
*
* @since 2.0
*/
SERVER_SHUTDOWN,
public static final SaveCause SERVER_SHUTDOWN = of("SERVER_SHUTDOWN");
/**
* Indicates data was saved by editing inventory contents via the {@code /inventory} command
*
* @since 2.0
*/
INVENTORY_COMMAND,
public static final SaveCause INVENTORY_COMMAND = of("INVENTORY_COMMAND");
/**
* Indicates data was saved by editing Ender Chest contents via the {@code /enderchest} command
*
* @since 2.0
*/
ENDERCHEST_COMMAND,
public static final SaveCause ENDERCHEST_COMMAND = of("ENDERCHEST_COMMAND");
/**
* Indicates data was saved by restoring it from a previous version
*
* @since 2.0
*/
BACKUP_RESTORE,
public static final SaveCause BACKUP_RESTORE = of("BACKUP_RESTORE");
/**
* Indicates data was saved by an API call
*
* @since 2.0
*/
API,
public static final SaveCause API = of("API");
/**
* Indicates data was saved from being imported from MySQLPlayerDataBridge
*
* @since 2.0
*/
MPDB_MIGRATION,
public static final SaveCause MPDB_MIGRATION = of("MPDB_MIGRATION");
/**
* Indicates data was saved from being imported from a legacy version (v1.x -> v2.x)
*
* @since 2.0
*/
LEGACY_MIGRATION,
public static final SaveCause LEGACY_MIGRATION = of("LEGACY_MIGRATION");
/**
* Indicates data was saved from being imported from a legacy version (v2.x -> v3.x)
*
* @since 3.0
*/
CONVERTED_FROM_V2;
public static final SaveCause CONVERTED_FROM_V2 = of("CONVERTED_FROM_V2");
@NotNull
public String getDisplayName() {
return name().toLowerCase(Locale.ENGLISH);
private final String name;
/**
* Get or create a {@link SaveCause} from a name
*
* @param name the name to be displayed
* @return the cause
*/
@NotNull
public static SaveCause of(@NotNull String name) {
return new SaveCause(name.length() > 32 ? name.substring(0, 31) : name);
}
@NotNull
public String getLocale(@NotNull HuskSync plugin) {
return plugin.getLocales().getRawLocale("save_cause_" + name().toLowerCase())
return plugin.getLocales()
.getRawLocale("save_cause_" + name().toLowerCase(Locale.ENGLISH))
.orElse(getDisplayName());
}
@NotNull
public static SaveCause[] values() {
return new SaveCause[]{
DISCONNECT, WORLD_SAVE, DEATH, SERVER_SHUTDOWN, INVENTORY_COMMAND, ENDERCHEST_COMMAND,
BACKUP_RESTORE, API, MPDB_MIGRATION, LEGACY_MIGRATION, CONVERTED_FROM_V2
};
}
}
/**
* Represents the cause of a player having their data updated.
* A string wrapper, for identifying the cause of a player data update.
* </p>
* Cause names have a max length of 32 characters.
*/
public enum UpdateCause {
@Accessors(fluent = true)
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class UpdateCause implements Cause {
/**
* Indicates the data was updated by a synchronization process
*
* @since 3.0
*/
SYNCHRONIZED("synchronization_complete", "synchronization_failed"),
public static final UpdateCause SYNCHRONIZED = of("SYNCHRONIZED",
"synchronization_complete", "synchronization_failed"
);
/**
* Indicates the data was updated by a user joining the server
*
* @since 3.0
*/
NEW_USER("user_registration_complete", null),
public static final UpdateCause NEW_USER = of("NEW_USER",
"user_registration_complete", null
);
/**
* Indicates the data was updated by a data update process (management command, API, etc.)
*
* @since 3.0
*/
UPDATED("data_update_complete", "data_update_failed");
public static final UpdateCause UPDATED = of("UPDATED",
"data_update_complete", "data_update_failed"
);
private final String completedLocale;
private final String failureLocale;
/**
* Indicates data was saved by an API call
*
* @since 3.3.3
*/
public static final UpdateCause API = of("API");
UpdateCause(@Nullable String completedLocale, @Nullable String failureLocale) {
this.completedLocale = completedLocale;
this.failureLocale = failureLocale;
@NotNull
private final String name;
@Nullable
private String completedLocale;
@Nullable
private String failureLocale;
/**
* Get or create a {@link UpdateCause} from a name and completed/failure locales
*
* @param name the name to be displayed
* @param completedLocale the locale to be displayed on successful update,
* or {@code null} if none is to be shown
* @param failureLocale the locale to be displayed on a failed update,
* or {@code null} if none is to be shown
* @return the cause
*/
public static UpdateCause of(@NotNull String name, @Nullable String completedLocale,
@Nullable String failureLocale) {
return new UpdateCause(
name.length() > 32 ? name.substring(0, 31) : name,
completedLocale, failureLocale
);
}
/**
* Get or create a {@link UpdateCause} from a name
*
* @param name the name to be displayed
* @return the cause
*/
@NotNull
public static UpdateCause of(@NotNull String name) {
return of(name, null, null);
}
/**
* Get the message to be displayed when a user's data is successfully updated.
*
* @param plugin plugin instance
* @return the message
*/
public Optional<MineDown> getCompletedLocale(@NotNull HuskSync plugin) {
if (completedLocale != null) {
return plugin.getLocales().getLocale(completedLocale);
if (completedLocale() != null) {
return Optional.of(plugin.getLocales().getLocale(completedLocale())
.orElse(plugin.getLocales().format(getDisplayName())));
}
return Optional.empty();
}
/**
* Get the message to be displayed when a user's data fails to update.
*
* @param plugin plugin instance
* @return the message
*/
public Optional<MineDown> getFailedLocale(@NotNull HuskSync plugin) {
if (failureLocale != null) {
return plugin.getLocales().getLocale(failureLocale);
if (failureLocale() != null) {
return Optional.of(plugin.getLocales().getLocale(failureLocale())
.orElse(plugin.getLocales().format(failureLocale())));
}
return Optional.empty();
}
@NotNull
public static UpdateCause[] values() {
return new UpdateCause[]{
SYNCHRONIZED, NEW_USER, UPDATED
};
}
}
}

View File

@@ -23,8 +23,6 @@ import lombok.Getter;
import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.data.DataSnapshot.SaveCause;
import net.william278.husksync.data.UserDataHolder;
import net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
@@ -33,6 +31,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.*;
import java.util.function.BiConsumer;
/**
* An abstract representation of the plugin database, storing player data.
@@ -156,42 +155,23 @@ public abstract class Database {
@Blocking
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid);
/**
* Save user data to the database
* </p>
* This will remove the oldest data for the user if the amount of data exceeds the limit as configured
*
* @param user The user to add data for
* @param snapshot The {@link DataSnapshot} to set.
* The implementation should version it with a random UUID and the current timestamp during insertion.
* @see UserDataHolder#createSnapshot(SaveCause)
*/
@Blocking
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
if (snapshot.getSaveCause() != SaveCause.SERVER_SHUTDOWN) {
plugin.fireEvent(
plugin.getDataSaveEvent(user, snapshot),
(event) -> this.addAndRotateSnapshot(user, snapshot)
);
return;
}
this.addAndRotateSnapshot(user, snapshot);
}
/**
* <b>Internal</b> - Save user data to the database. This will:
* Save user data to the database, doing the following (in order):
* <ol>
* <li>Delete their most recent snapshot, if it was created before the backup frequency time</li>
* <li>Create the snapshot</li>
* <li>Rotate snapshot backups</li>
* </ol>
* This is an expensive blocking method and should be run off the main thread.
*
* @param user The user to add data for
* @param snapshot The {@link DataSnapshot} to set.
* @apiNote Prefer {@link net.william278.husksync.sync.DataSyncer#saveData(User, DataSnapshot.Packed, BiConsumer)}.
* </p>This method will not fire the {@link net.william278.husksync.event.DataSaveEvent}
*/
@Blocking
private void addAndRotateSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot.Packed snapshot) {
final int backupFrequency = plugin.getSettings().getSynchronization().getSnapshotBackupFrequency();
if (!snapshot.isPinned() && backupFrequency > 0) {
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));

View File

@@ -81,7 +81,7 @@ public abstract class EventListener {
}
usersInWorld.stream()
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
.forEach(user -> plugin.getDatabase().addSnapshot(
.forEach(user -> plugin.getDataSyncer().saveData(
user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE)
));
}
@@ -101,7 +101,7 @@ public abstract class EventListener {
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
plugin.getDatabase().addSnapshot(user, snapshot);
plugin.getDataSyncer().saveData(user, snapshot);
}
/**
@@ -123,7 +123,9 @@ public abstract class EventListener {
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
.forEach(user -> {
plugin.lockPlayer(user.getUuid());
plugin.getDatabase().addSnapshot(user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN));
plugin.getDataSyncer().saveData(
user, user.createSnapshot(DataSnapshot.SaveCause.SERVER_SHUTDOWN), null
);
});
// Close outstanding connections
@@ -168,7 +170,6 @@ public abstract class EventListener {
return Map.entry(name().toLowerCase(), defaultPriority.name());
}
@SuppressWarnings("unchecked")
@NotNull
public static Map<String, String> getDefaults() {

View File

@@ -255,6 +255,18 @@ public class RedisManager extends JedisPubSub {
}
}
@Blocking
public void clearUserData(@NotNull User user) {
try (Jedis jedis = jedisPool.getResource()) {
jedis.del(
getKey(RedisKeyType.LATEST_SNAPSHOT, user.getUuid(), clusterId)
);
plugin.debug(String.format("[%s] Cleared %s on Redis", user.getUsername(), RedisKeyType.LATEST_SNAPSHOT));
} catch (Throwable e) {
plugin.log(Level.SEVERE, "An exception occurred clearing user data on Redis", e);
}
}
@Blocking
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
try (Jedis jedis = jedisPool.getResource()) {

View File

@@ -22,14 +22,20 @@ package net.william278.husksync.sync;
import net.william278.husksync.HuskSync;
import net.william278.husksync.api.HuskSyncAPI;
import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.database.Database;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.Task;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
@@ -87,6 +93,58 @@ public abstract class DataSyncer {
*/
public abstract void saveUserData(@NotNull OnlineUser user);
/**
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
*
* @param user the user to save the data for
* @param data the data to save
* @param after a consumer to run after data has been saved. Will be run async (off the main thread).
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is cancelled.
* Note that this method can also edit the data before saving it.
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if the
* save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}.
* @since 3.3.2
*/
@Blocking
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data,
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
if (data.getSaveCause() == DataSnapshot.SaveCause.SERVER_SHUTDOWN) {
addSnapshotToDatabase(user, data, after);
return;
}
plugin.fireEvent(
plugin.getDataSaveEvent(user, data),
(event) -> addSnapshotToDatabase(user, data, after)
);
}
/**
* Save a {@link DataSnapshot.Packed user's data snapshot} to the database,
* first firing the {@link net.william278.husksync.event.DataSaveEvent}. This will not update data on Redis.
*
* @param user the user to save the data for
* @param data the data to save
* @apiNote Data will not be saved if the {@link net.william278.husksync.event.DataSaveEvent} is cancelled.
* Note that this method can also edit the data before saving it.
* @implNote Note that the {@link net.william278.husksync.event.DataSaveEvent} will <b>not</b> be fired if the
* save cause is {@link DataSnapshot.SaveCause#SERVER_SHUTDOWN}.
* @since 3.3.3
*/
public void saveData(@NotNull User user, @NotNull DataSnapshot.Packed data) {
saveData(user, data, null);
}
// Adds a snapshot to the database and runs the after consumer
@Blocking
private void addSnapshotToDatabase(@NotNull User user, @NotNull DataSnapshot.Packed data,
@Nullable BiConsumer<User, DataSnapshot.Packed> after) {
getDatabase().addSnapshot(user, data);
if (after != null) {
after.accept(user, data);
}
}
// Calculates the max attempts the system should listen for user data for based on the latency value
private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + (
@@ -98,7 +156,7 @@ public abstract class DataSyncer {
// Set a user's data from the database, or set them as a new user
@ApiStatus.Internal
protected void setUserFromDatabase(@NotNull OnlineUser user) {
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse(
getDatabase().getLatestSnapshot(user).ifPresentOrElse(
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
);
@@ -139,6 +197,16 @@ public abstract class DataSyncer {
task.get().run();
}
@NotNull
protected RedisManager getRedis() {
return plugin.getRedisManager();
}
@NotNull
protected Database getDatabase() {
return plugin.getDatabase();
}
/**
* Represents the different available default modes of {@link DataSyncer}
*

View File

@@ -39,7 +39,7 @@ public class DelayDataSyncer extends DataSyncer {
plugin.runAsyncDelayed(
() -> {
// Fetch from the database if the user isn't changing servers
if (!plugin.getRedisManager().getUserServerSwitch(user)) {
if (!getRedis().getUserServerSwitch(user)) {
this.setUserFromDatabase(user);
return;
}
@@ -47,7 +47,7 @@ public class DelayDataSyncer extends DataSyncer {
// Listen for the data to be updated
this.listenForRedisData(
user,
() -> plugin.getRedisManager().getUserData(user).map(data -> {
() -> getRedis().getUserData(user).map(data -> {
user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED);
return true;
}).orElse(false)
@@ -58,12 +58,13 @@ public class DelayDataSyncer extends DataSyncer {
}
@Override
public void saveUserData(@NotNull OnlineUser user) {
public void saveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> {
plugin.getRedisManager().setUserServerSwitch(user);
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_10_SECONDS);
plugin.getDatabase().addSnapshot(user, data);
getRedis().setUserServerSwitch(onlineUser);
saveData(
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
(user, data) -> getRedis().setUserData(user, data, RedisKeyType.TTL_10_SECONDS)
);
});
}

View File

@@ -33,21 +33,21 @@ public class LockstepDataSyncer extends DataSyncer {
@Override
public void initialize() {
plugin.getRedisManager().clearUsersCheckedOutOnServer();
getRedis().clearUsersCheckedOutOnServer();
}
@Override
public void terminate() {
plugin.getRedisManager().clearUsersCheckedOutOnServer();
getRedis().clearUsersCheckedOutOnServer();
}
// Consume their data when they are checked in
@Override
public void setUserData(@NotNull OnlineUser user) {
this.listenForRedisData(user, () -> {
if (plugin.getRedisManager().getUserCheckedOut(user).isEmpty()) {
plugin.getRedisManager().setUserCheckedOut(user, true);
plugin.getRedisManager().getUserData(user).ifPresentOrElse(
if (getRedis().getUserCheckedOut(user).isEmpty()) {
getRedis().setUserCheckedOut(user, true);
getRedis().getUserData(user).ifPresentOrElse(
data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> this.setUserFromDatabase(user)
);
@@ -58,12 +58,16 @@ public class LockstepDataSyncer extends DataSyncer {
}
@Override
public void saveUserData(@NotNull OnlineUser user) {
public void saveUserData(@NotNull OnlineUser onlineUser) {
plugin.runAsync(() -> {
final DataSnapshot.Packed data = user.createSnapshot(DataSnapshot.SaveCause.DISCONNECT);
plugin.getRedisManager().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
plugin.getRedisManager().setUserCheckedOut(user, false);
plugin.getDatabase().addSnapshot(user, data);
getRedis().setUserServerSwitch(onlineUser);
saveData(
onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT),
(user, data) -> {
getRedis().setUserData(user, data, RedisKeyType.TTL_1_YEAR);
getRedis().setUserCheckedOut(user, false);
}
);
});
}

View File

@@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8'
org.gradle.daemon=true
javaVersion=17
plugin_version=3.3.1
plugin_version=3.3.3
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system