9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-25 01:29:19 +00:00

Compare commits

...

14 Commits
3.3 ... 3.3.2

Author SHA1 Message Date
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
William
325fac41bf deps: bump junit to 5.10.2 2024-02-05 15:38:44 +00:00
William
87377bffc1 docs: update FAQs 2024-02-05 10:38:00 +00:00
William
c6fb7fb10f fix: preserve order of saved items to keep, close #186 2024-02-02 23:01:41 +00:00
William
c2ae9bd20a build: bump to 3.3.1 2024-02-02 22:24:47 +00:00
William
e580c4f2bd fix: LOCKSTEP preventing offline inv updates, close #229 2024-02-02 22:24:27 +00:00
dependabot[bot]
dabd9bc57d ci: bump gradle/gradle-build-action from 2 to 3 (#235)
Bumps [gradle/gradle-build-action](https://github.com/gradle/gradle-build-action) from 2 to 3.
- [Release notes](https://github.com/gradle/gradle-build-action/releases)
- [Commits](https://github.com/gradle/gradle-build-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: gradle/gradle-build-action
  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-01-31 09:45:44 +00:00
22 changed files with 215 additions and 106 deletions

View File

@@ -24,7 +24,7 @@ jobs:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: 'Build with Gradle 🏗️' - name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
with: with:
arguments: build test publish arguments: build test publish
env: env:

View File

@@ -20,7 +20,7 @@ jobs:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: 'Build with Gradle 🏗️' - name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
with: with:
arguments: test arguments: test
- name: 'Publish Test Report 📊' - name: 'Publish Test Report 📊'

View File

@@ -20,7 +20,7 @@ jobs:
java-version: '17' java-version: '17'
distribution: 'temurin' distribution: 'temurin'
- name: 'Build with Gradle 🏗️' - name: 'Build with Gradle 🏗️'
uses: gradle/gradle-build-action@v2 uses: gradle/gradle-build-action@v3
with: with:
arguments: build test publish arguments: build test publish
env: env:

View File

@@ -78,9 +78,9 @@ allprojects {
} }
dependencies { dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.1' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.1' testImplementation 'org.junit.jupiter:junit-jupiter-params:5.10.2'
testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.1' testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.10.2'
} }
test { test {

View File

@@ -15,7 +15,7 @@ dependencies {
compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT' compileOnly 'org.spigotmc:spigot-api:1.17.1-R0.1-SNAPSHOT'
compileOnly 'org.projectlombok:lombok:1.18.30' compileOnly 'org.projectlombok:lombok:1.18.30'
compileOnly 'commons-io:commons-io:2.15.1' 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 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT'
compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0' compileOnly 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0'
compileOnly 'com.zaxxer:HikariCP:5.1.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.BukkitTask;
import net.william278.husksync.util.LegacyConverter; import net.william278.husksync.util.LegacyConverter;
import org.bstats.bukkit.Metrics; import org.bstats.bukkit.Metrics;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.map.MapView; import org.bukkit.map.MapView;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
@@ -229,7 +228,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
@NotNull @NotNull
public Set<OnlineUser> getOnlineUsers() { public Set<OnlineUser> getOnlineUsers() {
return Bukkit.getOnlinePlayers().stream() return getServer().getOnlinePlayers().stream()
.map(player -> BukkitUser.adapt(player, this)) .map(player -> BukkitUser.adapt(player, this))
.collect(Collectors.toSet()); .collect(Collectors.toSet());
} }
@@ -237,7 +236,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
@NotNull @NotNull
public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) { public Optional<OnlineUser> getOnlineUser(@NotNull UUID uuid) {
final Player player = Bukkit.getPlayer(uuid); final Player player = getServer().getPlayer(uuid);
if (player == null) { if (player == null) {
return Optional.empty(); return Optional.empty();
} }
@@ -253,12 +252,10 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@NotNull @NotNull
@Override @Override
public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) { public Map<Identifier, Data> getPlayerCustomDataStore(@NotNull OnlineUser user) {
if (playerCustomDataStore.containsKey(user.getUuid())) { return playerCustomDataStore.compute(
return playerCustomDataStore.get(user.getUuid()); user.getUuid(),
} (uuid, data) -> data == null ? Maps.newHashMap() : data
final Map<Identifier, Data> data = Maps.newHashMap(); );
playerCustomDataStore.put(user.getUuid(), data);
return data;
} }
@Override @Override
@@ -269,7 +266,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
public boolean isDependencyLoaded(@NotNull String name) { public boolean isDependencyLoaded(@NotNull String name) {
return Bukkit.getPluginManager().getPlugin(name) != null; return getServer().getPluginManager().getPlugin(name) != null;
} }
// Register bStats metrics // Register bStats metrics
@@ -303,7 +300,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@NotNull @NotNull
@Override @Override
public Version getMinecraftVersion() { public Version getMinecraftVersion() {
return Version.fromString(Bukkit.getBukkitVersion()); return Version.fromString(getServer().getBukkitVersion());
} }
@NotNull @NotNull
@@ -347,7 +344,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
@Override @Override
@NotNull @NotNull
public HuskSync getPlugin() { public BukkitHuskSync getPlugin() {
return this; return this;
} }

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ dependencies {
api 'commons-io:commons-io:2.15.1' api 'commons-io:commons-io:2.15.1'
api 'org.apache.commons:commons-text:1.11.0' api 'org.apache.commons:commons-text:1.11.0'
api 'de.themoep:minedown-adventure:1.7.2-SNAPSHOT' 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.google.code.gson:gson:2.10.1'
api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2' api 'com.fatboyindustrial.gson-javatime-serialisers:gson-javatime-serialisers:1.1.2'
api 'com.github.Exlll.ConfigLib:configlib-yaml:v4.4.0' 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 net.william278.husksync.user.User;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
/** /**
* The common implementation of the HuskSync API, containing cross-platform API calls. * 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 user The user to save the data for
* @param snapshot The snapshot to save * @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 * @since 3.0
*/ */
public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) { public void addSnapshot(@NotNull User user, @NotNull DataSnapshot snapshot) {
plugin.runAsync(() -> plugin.getDatabase().addSnapshot( this.addSnapshot(user, snapshot, null);
user, snapshot instanceof DataSnapshot.Unpacked unpacked
? unpacked.pack(plugin) : (DataSnapshot.Packed) snapshot
));
} }
/** /**

View File

@@ -23,6 +23,8 @@ import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data; import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -70,8 +72,8 @@ public class EnderChestCommand extends ItemsCommand {
// Creates a new snapshot with the updated enderChest // Creates a new snapshot with the updated enderChest
@SuppressWarnings("DuplicatedCode") @SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) { private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
@@ -87,8 +89,13 @@ public class EnderChestCommand extends ItemsCommand {
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.ENDERCHEST_COMMAND)
); );
}); });
plugin.getDatabase().addSnapshot(user, snapshot);
plugin.getRedisManager().sendUserDataUpdate(user, snapshot); // Save data
final RedisManager redis = plugin.getRedisManager();
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

@@ -23,6 +23,8 @@ import de.themoep.minedown.adventure.MineDown;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.data.Data; import net.william278.husksync.data.Data;
import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.data.DataSnapshot;
import net.william278.husksync.redis.RedisKeyType;
import net.william278.husksync.redis.RedisManager;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -70,8 +72,8 @@ public class InventoryCommand extends ItemsCommand {
// Creates a new snapshot with the updated inventory // Creates a new snapshot with the updated inventory
@SuppressWarnings("DuplicatedCode") @SuppressWarnings("DuplicatedCode")
private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User user) { private void updateItems(@NotNull OnlineUser viewer, @NotNull Data.Items.Items items, @NotNull User holder) {
final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(user); final Optional<DataSnapshot.Packed> latestData = plugin.getDatabase().getLatestSnapshot(holder);
if (latestData.isEmpty()) { if (latestData.isEmpty()) {
plugin.getLocales().getLocale("error_no_data_to_display") plugin.getLocales().getLocale("error_no_data_to_display")
.ifPresent(viewer::sendMessage); .ifPresent(viewer::sendMessage);
@@ -87,8 +89,13 @@ public class InventoryCommand extends ItemsCommand {
plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND) plugin.getSettings().getSynchronization().doAutoPin(DataSnapshot.SaveCause.INVENTORY_COMMAND)
); );
}); });
plugin.getDatabase().addSnapshot(user, snapshot);
plugin.getRedisManager().sendUserDataUpdate(user, snapshot); // Save data
final RedisManager redis = plugin.getRedisManager();
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.HuskSync;
import net.william278.husksync.data.DataSnapshot; 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.CommandUser;
import net.william278.husksync.user.User; import net.william278.husksync.user.User;
import net.william278.husksync.util.DataDumper; import net.william278.husksync.util.DataDumper;
@@ -110,13 +112,14 @@ public class UserDataCommand extends Command implements TabProvider {
return; return;
} }
// Delete user data by specified UUID // Delete user data by specified UUID and clear their data cache
final UUID version = optionalUuid.get(); final UUID version = optionalUuid.get();
if (!plugin.getDatabase().deleteSnapshot(user, version)) { if (!plugin.getDatabase().deleteSnapshot(user, version)) {
plugin.getLocales().getLocale("error_invalid_version_uuid") plugin.getLocales().getLocale("error_invalid_version_uuid")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; return;
} }
plugin.getRedisManager().clearUserData(user);
plugin.getLocales().getLocale("data_deleted", plugin.getLocales().getLocale("data_deleted",
version.toString().split("-")[0], version.toString().split("-")[0],
@@ -152,11 +155,14 @@ public class UserDataCommand extends Command implements TabProvider {
); );
})); }));
// Set the user's data and send a message // Save data
plugin.getDatabase().addSnapshot(user, data); final RedisManager redis = plugin.getRedisManager();
plugin.getRedisManager().sendUserDataUpdate(user, data); plugin.getDataSyncer().saveData(user, data, (u, s) -> {
plugin.getLocales().getLocale("data_restored", user.getUsername(), user.getUuid().toString(), redis.getUserData(u).ifPresent(d -> redis.setUserData(u, s, RedisKeyType.TTL_1_YEAR));
data.getShortId(), data.getId().toString()).ifPresent(executor::sendMessage); redis.sendUserDataUpdate(u, s);
plugin.getLocales().getLocale("data_restored", u.getUsername(), u.getUuid().toString(),
s.getShortId(), s.getId().toString()).ifPresent(executor::sendMessage);
});
} }
case "pin" -> { case "pin" -> {

View File

@@ -23,8 +23,6 @@ import lombok.Getter;
import net.william278.husksync.HuskSync; import net.william278.husksync.HuskSync;
import net.william278.husksync.config.Settings; import net.william278.husksync.config.Settings;
import net.william278.husksync.data.DataSnapshot; 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 net.william278.husksync.user.User;
import org.jetbrains.annotations.Blocking; import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@@ -33,6 +31,7 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.function.BiConsumer;
/** /**
* An abstract representation of the plugin database, storing player data. * An abstract representation of the plugin database, storing player data.
@@ -156,42 +155,23 @@ public abstract class Database {
@Blocking @Blocking
public abstract boolean deleteSnapshot(@NotNull User user, @NotNull UUID versionUuid); 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> * <ol>
* <li>Delete their most recent snapshot, if it was created before the backup frequency time</li> * <li>Delete their most recent snapshot, if it was created before the backup frequency time</li>
* <li>Create the snapshot</li> * <li>Create the snapshot</li>
* <li>Rotate snapshot backups</li> * <li>Rotate snapshot backups</li>
* </ol> * </ol>
* This is an expensive blocking method and should be run off the main thread.
* *
* @param user The user to add data for * @param user The user to add data for
* @param snapshot The {@link DataSnapshot} to set. * @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 @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(); final int backupFrequency = plugin.getSettings().getSynchronization().getSnapshotBackupFrequency();
if (!snapshot.isPinned() && backupFrequency > 0) { if (!snapshot.isPinned() && backupFrequency > 0) {
this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency)); this.rotateLatestSnapshot(user, snapshot.getTimestamp().minusHours(backupFrequency));

View File

@@ -81,8 +81,8 @@ public abstract class EventListener {
} }
usersInWorld.stream() usersInWorld.stream()
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc()) .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) user, user.createSnapshot(DataSnapshot.SaveCause.WORLD_SAVE), null
)); ));
} }
@@ -101,7 +101,7 @@ public abstract class EventListener {
final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH); final DataSnapshot.Packed snapshot = user.createSnapshot(DataSnapshot.SaveCause.DEATH);
snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items)))); snapshot.edit(plugin, (data -> data.getInventory().ifPresent(inventory -> inventory.setContents(items))));
plugin.getDatabase().addSnapshot(user, snapshot); plugin.getDataSyncer().saveData(user, snapshot, (u, d) -> plugin.getRedisManager().sendUserDataUpdate(u, d));
} }
/** /**
@@ -123,7 +123,9 @@ public abstract class EventListener {
.filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc()) .filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc())
.forEach(user -> { .forEach(user -> {
plugin.lockPlayer(user.getUuid()); 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 // Close outstanding connections

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 @Blocking
public void setUserCheckedOut(@NotNull User user, boolean checkedOut) { public void setUserCheckedOut(@NotNull User user, boolean checkedOut) {
try (Jedis jedis = jedisPool.getResource()) { 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.HuskSync;
import net.william278.husksync.api.HuskSyncAPI; import net.william278.husksync.api.HuskSyncAPI;
import net.william278.husksync.data.DataSnapshot; 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.OnlineUser;
import net.william278.husksync.user.User;
import net.william278.husksync.util.Task; import net.william278.husksync.util.Task;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Blocking;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -87,6 +93,42 @@ public abstract class DataSyncer {
*/ */
public abstract void saveUserData(@NotNull OnlineUser user); 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)
);
}
// 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 // Calculates the max attempts the system should listen for user data for based on the latency value
private long getMaxListenAttempts() { private long getMaxListenAttempts() {
return BASE_LISTEN_ATTEMPTS + ( return BASE_LISTEN_ATTEMPTS + (
@@ -98,7 +140,7 @@ public abstract class DataSyncer {
// Set a user's data from the database, or set them as a new user // Set a user's data from the database, or set them as a new user
@ApiStatus.Internal @ApiStatus.Internal
protected void setUserFromDatabase(@NotNull OnlineUser user) { protected void setUserFromDatabase(@NotNull OnlineUser user) {
plugin.getDatabase().getLatestSnapshot(user).ifPresentOrElse( getDatabase().getLatestSnapshot(user).ifPresentOrElse(
snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED), snapshot -> user.applySnapshot(snapshot, DataSnapshot.UpdateCause.SYNCHRONIZED),
() -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin) () -> user.completeSync(true, DataSnapshot.UpdateCause.NEW_USER, plugin)
); );
@@ -139,6 +181,16 @@ public abstract class DataSyncer {
task.get().run(); 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} * Represents the different available default modes of {@link DataSyncer}
* *

View File

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

View File

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

View File

@@ -46,23 +46,26 @@ This approach is able to dramatically improve both synchronization performance a
This is a very common request, but there's a good reason why HuskSync does not support this. This is a very common request, but there's a good reason why HuskSync does not support this.
The Vault API is designed to be a central "Vault" for storing user data. It's the role of economy plugins that *implement* vault to handle the data storage -- and, by extension, synchronization cross-server. Plugins that *hook into* Vault then expect to be able to use the Vault API to get the player's latest economy balance and data. Vault is a plugin that provides a common API for developers to do two things:
Plugins such as MySQLPlayerDataBridge that support synchronizing Vault *hook into* Vault and as a result can violate this expectation&mdash;plugins that expect Vault to return the latest user data no longer can. As a result, plugins like MySQLPlayerDataBridge have to provide lots of manual hooks and tweaks for individual plugins to ensure compatibility. 1. Developers can _implement_ Vault to create economy plugins
2. Developers can _target_ Vault to modify and check economy balances without having to write code to hook into individual economy plugins
This causes all sorts of compatibility issues with unsupported plugins and increases plugin size and update workload. In essence, Vault is beneficial as it allows developers to write less code. A developer only needs to write code that targets the Vault API when you need to do stuff with player economy balances.
As a result, I recommend using an economy plugin (that directly *implements* the Vault API), that works cross-server. XConomy is a popular choice for this, which I have personally had a good experience with in the past. _Vault itself, however, is not an Economy plugin_. The developers of Economy plugins that _implement_ are responsible for writing the implementation code and database systems for creating player economy accounts and updating balances. By extension, this also means it is the responsibility of Economy plugin developers to implement Vault's API in a way that allows that data to be synchronized cross-server; Vault itself does not contain API for doing so.
Most Economy plugins do not support doing this, however, as cross-server support isn't (and historically hasn't) been a priority. _MySQLPlayerDataBridge_ allows you to workaround this and synchronize Vault balances &mdash; but as detailed above, since Vault itself is not an economy plugin, the way this works is MySQLPlayerDataBridge has to provide and continually maintain a bespoke laundry list of manual, individual hooks and tweaks for both Economy plugins that _implement_ Vault and other plugins that _target_ Vault.
Implementing a similar system in HuskSync would considerably increase the size of the codebase, lengthen update times, and decrease overall system stability. The much better solution is to use an Economy plugin that _implements_ Vault in a way that works cross-server.
Indeed, there exist economy plugins &mdash; such as [XConomy](https://github.com/YiC200333/XConomy) and [RedisEconomy](https://github.com/Emibergo02/RedisEconomy) which do just this, and this is my recommended solution. Need to move from an incompatible Economy plugin? Vault provides methods for transferring balances between Economy plugins (`/vault-convert`).
</details> </details>
<details> <details>
<summary>&nbsp;<b>Is this better than MySQLPlayerDataBridge?</b></summary> <summary>&nbsp;<b>Is this better than MySQLPlayerDataBridge?</b></summary>
I can't provide a fair answer to this question! What I can say is that your mileage may vary. The performance improvements offered by HuskSync's synchronization method will depend on your network environment and the economies of scale that come with your player count. I can't provide a fair answer to this question! What I can say is that your mileage will of course vary.
With that said, servers running plugins or mods that make use of custom items (such as MMOItems, SlimeFun) are not supported by HuskSync and so MySQLPlayerDataBridge may be a better choice for you. The performance improvements offered by HuskSync's synchronization method will depend on your network environment and the economies of scale that come with your player count. In terms of featureset, HuskSync does feature greater rollback and snapshot backup/management features if this is something you are looking for.
</details>
A migrator from MPDB is built-in to HuskSync.
</details>

View File

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

View File

@@ -19,14 +19,17 @@
package net.william278.husksync.listener; package net.william278.husksync.listener;
import com.google.common.collect.Lists;
import net.william278.husksync.BukkitHuskSync; import net.william278.husksync.BukkitHuskSync;
import net.william278.husksync.data.BukkitData; import net.william278.husksync.data.BukkitData;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import net.william278.husksync.user.OnlineUser; import net.william278.husksync.user.OnlineUser;
import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Iterator;
import java.util.List; import java.util.List;
import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings; import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings;
@@ -57,7 +60,7 @@ public class PaperEventListener extends BukkitEventListener {
final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT; final int maxInventorySize = BukkitData.Items.Inventory.INVENTORY_SLOT_COUNT;
final List<ItemStack> itemsToSave = switch (settings.getItemsToSave()) { final List<ItemStack> itemsToSave = switch (settings.getItemsToSave()) {
case DROPS -> event.getDrops(); case DROPS -> event.getDrops();
case ITEMS_TO_KEEP -> event.getItemsToKeep(); case ITEMS_TO_KEEP -> preserveOrder(event.getEntity().getInventory(), event.getItemsToKeep());
}; };
if (itemsToSave.size() > maxInventorySize) { if (itemsToSave.size() > maxInventorySize) {
itemsToSave.subList(maxInventorySize, itemsToSave.size()).clear(); itemsToSave.subList(maxInventorySize, itemsToSave.size()).clear();
@@ -65,4 +68,22 @@ public class PaperEventListener extends BukkitEventListener {
super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave)); super.saveOnPlayerDeath(user, BukkitData.Items.ItemArray.adapt(itemsToSave));
} }
@NotNull
private List<ItemStack> preserveOrder(@NotNull PlayerInventory inventory, @NotNull List<ItemStack> toKeep) {
final List<ItemStack> preserved = Lists.newArrayList();
final List<ItemStack> items = Lists.newArrayList(inventory.getContents());
for (ItemStack item : toKeep) {
final Iterator<ItemStack> iterator = items.iterator();
while (iterator.hasNext()) {
final ItemStack originalItem = iterator.next();
if (originalItem != null && originalItem.equals(item)) {
preserved.add(originalItem);
iterator.remove();
break;
}
}
}
return preserved;
}
} }