diff --git a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java index f8686c64..ab7bde87 100644 --- a/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java +++ b/bukkit/src/main/java/net/william278/husksync/BukkitHuskSync.java @@ -99,6 +99,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S private final Map mapViews = Maps.newConcurrentMap(); private final List availableMigrators = Lists.newArrayList(); private final Set lockedPlayers = Sets.newConcurrentHashSet(); + private final Set disconnectingPlayers = Sets.newConcurrentHashSet(); private boolean disabling; private Gson gson; diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java index f6dcacb7..84812fe8 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitUserDataHolder.java @@ -31,7 +31,11 @@ public interface BukkitUserDataHolder extends UserDataHolder { @Override default Optional getData(@NotNull Identifier id) { - if (!id.isCustom()) { + if (id.isCustom()) { + return Optional.ofNullable(getCustomDataStore().get(id)); + } + + try { return switch (id.getKeyValue()) { case "inventory" -> getInventory(); case "ender_chest" -> getEnderChest(); @@ -48,8 +52,10 @@ public interface BukkitUserDataHolder extends UserDataHolder { case "persistent_data" -> getPersistentData(); default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id)); }; + } catch (Throwable e) { + getPlugin().debug("Failed to get data for key: " + id.asMinimalString(), e); + return Optional.empty(); } - return Optional.ofNullable(getCustomDataStore().get(id)); } @Override diff --git a/bukkit/src/main/java/net/william278/husksync/listener/PaperEventListener.java b/bukkit/src/main/java/net/william278/husksync/listener/PaperEventListener.java index cc921dde..3dc67349 100644 --- a/bukkit/src/main/java/net/william278/husksync/listener/PaperEventListener.java +++ b/bukkit/src/main/java/net/william278/husksync/listener/PaperEventListener.java @@ -44,6 +44,7 @@ public class PaperEventListener extends BukkitEventListener { } @Override + @SuppressWarnings("RedundantMethodOverride") public void onEnable() { getPlugin().getServer().getPluginManager().registerEvents(this, getPlugin()); lockedHandler.onEnable(); diff --git a/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java b/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java index 30dead7c..cad1aa97 100644 --- a/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java +++ b/bukkit/src/main/java/net/william278/husksync/user/BukkitUser.java @@ -57,8 +57,9 @@ public class BukkitUser extends OnlineUser implements BukkitUserDataHolder { } @Override - public boolean isOffline() { - return player == null || !player.isOnline(); + public boolean hasDisconnected() { + return getPlugin().getDisconnectingPlayers().contains(getUuid()) + || player == null || !player.isOnline(); } @Override diff --git a/common/src/main/java/net/william278/husksync/HuskSync.java b/common/src/main/java/net/william278/husksync/HuskSync.java index 53db1c02..ef67385f 100644 --- a/common/src/main/java/net/william278/husksync/HuskSync.java +++ b/common/src/main/java/net/william278/husksync/HuskSync.java @@ -20,6 +20,7 @@ package net.william278.husksync; import com.fatboyindustrial.gsonjavatime.Converters; +import com.google.common.collect.Maps; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import net.kyori.adventure.audience.Audience; @@ -140,7 +141,7 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider if (getPlayerCustomDataStore().containsKey(user.getUuid())) { return getPlayerCustomDataStore().get(user.getUuid()); } - final Map data = new HashMap<>(); + final Map data = Maps.newHashMap(); getPlayerCustomDataStore().put(user.getUuid(), data); return data; } @@ -315,6 +316,12 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider @NotNull Set getLockedPlayers(); + /** + * Get the set of UUIDs of players who are currently marked as disconnecting or disconnected + */ + @NotNull + Set getDisconnectingPlayers(); + default boolean isLocked(@NotNull UUID uuid) { return getLockedPlayers().contains(uuid); } diff --git a/common/src/main/java/net/william278/husksync/data/Identifier.java b/common/src/main/java/net/william278/husksync/data/Identifier.java index 425ed72f..94b40fe9 100644 --- a/common/src/main/java/net/william278/husksync/data/Identifier.java +++ b/common/src/main/java/net/william278/husksync/data/Identifier.java @@ -22,6 +22,7 @@ package net.william278.husksync.data; import lombok.*; import net.kyori.adventure.key.InvalidKeyException; import net.kyori.adventure.key.Key; +import net.kyori.adventure.key.KeyPattern; import org.intellij.lang.annotations.Subst; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -39,6 +40,9 @@ import java.util.stream.Stream; @Getter public class Identifier { + // Namespace for built-in identifiers + private static final @KeyPattern String DEFAULT_NAMESPACE = "husksync"; + // Built-in identifiers public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true); public static final Identifier INVENTORY = huskSync("inventory", true); @@ -93,8 +97,8 @@ public class Identifier { */ @NotNull public static Identifier from(@NotNull Key key, @NotNull Set dependencies) { - if (key.namespace().equals("husksync")) { - throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!"); + if (key.namespace().equals(DEFAULT_NAMESPACE)) { + throw new IllegalArgumentException("Cannot register with %s as key namespace!".formatted(key.namespace())); } return new Identifier(key, true, dependencies); } @@ -143,7 +147,7 @@ public class Identifier { @NotNull private static Identifier huskSync(@Subst("null") @NotNull String name, boolean configDefault) throws InvalidKeyException { - return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet()); + return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Collections.emptySet()); } // Return an identifier with a HuskSync namespace @@ -151,7 +155,7 @@ public class Identifier { private static Identifier huskSync(@Subst("null") @NotNull String name, @SuppressWarnings("SameParameterValue") boolean configDefault, @NotNull Dependency... dependents) throws InvalidKeyException { - return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents)); + return new Identifier(Key.key(DEFAULT_NAMESPACE, name), configDefault, Set.of(dependents)); } /** @@ -209,13 +213,30 @@ public class Identifier { * @return {@code false} if {@link #getKeyNamespace()} returns "husksync"; {@code true} otherwise */ public boolean isCustom() { - return !getKeyNamespace().equals("husksync"); + return !getKeyNamespace().equals(DEFAULT_NAMESPACE); + } + + /** + * Get the minimal string representation of this key. + *

+ * If the namespace of the key is {@link #DEFAULT_NAMESPACE}, only the key value will be returned. + * + * @return the minimal string key representation + * @since 3.8 + */ + @NotNull + public String asMinimalString() { + if (getKey().namespace().equals(DEFAULT_NAMESPACE)) { + return getKey().value(); + } + return getKey().asString(); } /** * Returns the identifier as a string (the key) * * @return the identifier as a string + * @since 3.0 */ @NotNull @Override @@ -224,19 +245,29 @@ public class Identifier { } /** - * Returns {@code true} if the given object is an identifier with the same key as this identifier + * Return whether this Identifier is equal to another Identifier * - * @param obj the object to compare - * @return {@code true} if the given object is an identifier with the same key as this identifier + * @param obj another object + * @return {@code true} if this identifier matches the identifier of {@code obj} + * @since 3.8 */ @Override public boolean equals(@Nullable Object obj) { - return obj instanceof Identifier other ? toString().equals(other.toString()) : super.equals(obj); + if (obj instanceof Identifier other) { + return asMinimalString().equals(other.asMinimalString()); + } + return false; } + /** + * Get the hash code of the Identifier (equivalent to {@link #asMinimalString()}->{@code #hashCode()} + * + * @return the hash code + * @since 3.8 + */ @Override public int hashCode() { - return key.toString().hashCode(); + return asMinimalString().hashCode(); } // Get the config entry for the identifier diff --git a/common/src/main/java/net/william278/husksync/data/UserDataHolder.java b/common/src/main/java/net/william278/husksync/data/UserDataHolder.java index 5f1cb9ff..3cce1952 100644 --- a/common/src/main/java/net/william278/husksync/data/UserDataHolder.java +++ b/common/src/main/java/net/william278/husksync/data/UserDataHolder.java @@ -75,6 +75,15 @@ public interface UserDataHolder extends DataHolder { return DataSnapshot.builder(getPlugin()).data(this.getData()).saveCause(saveCause).buildAndPack(); } + /** + * Returns whether data can be applied to the holder at this time + * + * @return {@code true} if data can be applied, otherwise false + */ + default boolean cannotApplySnapshot() { + return false; + } + /** * Deserialize and apply a data snapshot to this data owner *

@@ -90,9 +99,12 @@ public interface UserDataHolder extends DataHolder { * @since 3.0 */ default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer runAfter) { - final HuskSync plugin = getPlugin(); + if (cannotApplySnapshot()) { + return; + } // Unpack the snapshot + final HuskSync plugin = getPlugin(); final DataSnapshot.Unpacked unpacked; try { unpacked = snapshot.unpack(plugin); @@ -104,6 +116,10 @@ public interface UserDataHolder extends DataHolder { // Synchronously attempt to apply the snapshot plugin.runSync(() -> { + if (cannotApplySnapshot()) { + return; + } + try { for (Map.Entry entry : unpacked.getData().entrySet()) { final Identifier identifier = entry.getKey(); diff --git a/common/src/main/java/net/william278/husksync/listener/EventListener.java b/common/src/main/java/net/william278/husksync/listener/EventListener.java index a09a0347..78390a4c 100644 --- a/common/src/main/java/net/william278/husksync/listener/EventListener.java +++ b/common/src/main/java/net/william278/husksync/listener/EventListener.java @@ -25,9 +25,7 @@ import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.user.OnlineUser; import org.jetbrains.annotations.NotNull; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; import static net.william278.husksync.config.Settings.SynchronizationSettings.SaveOnDeathSettings; @@ -49,6 +47,7 @@ public abstract class EventListener { * @param user The {@link OnlineUser} to handle */ protected final void handlePlayerJoin(@NotNull OnlineUser user) { + plugin.getDisconnectingPlayers().remove(user.getUuid()); if (user.isNpc()) { return; } @@ -62,11 +61,17 @@ public abstract class EventListener { * @param user The {@link OnlineUser} to handle */ protected final void handlePlayerQuit(@NotNull OnlineUser user) { - if (user.isNpc() || plugin.isDisabling() || plugin.isLocked(user.getUuid())) { + // Check the user is a user, the plugin isn't disabling, then mark as disconnecting + if (user.isNpc() || plugin.isDisabling()) { return; } - plugin.lockPlayer(user.getUuid()); - plugin.getDataSyncer().syncSaveUserData(user); + plugin.getDisconnectingPlayers().add(user.getUuid()); + + // Lock, then save their data if the user is unlocked + if (!plugin.isLocked(user.getUuid())) { + plugin.lockPlayer(user.getUuid()); + plugin.getDataSyncer().syncSaveUserData(user); + } } /** @@ -79,7 +84,7 @@ public abstract class EventListener { return; } usersInWorld.stream() - .filter(user -> !plugin.isLocked(user.getUuid()) && !user.isNpc()) + .filter(user -> !user.isNpc() && !user.hasDisconnected() && !plugin.isLocked(user.getUuid())) .forEach(user -> plugin.getDataSyncer().saveCurrentUserData( user, DataSnapshot.SaveCause.WORLD_SAVE )); diff --git a/common/src/main/java/net/william278/husksync/redis/RedisManager.java b/common/src/main/java/net/william278/husksync/redis/RedisManager.java index a176237a..fe289edf 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisManager.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisManager.java @@ -158,7 +158,7 @@ public class RedisManager extends JedisPubSub { final RedisMessage redisMessage = RedisMessage.fromJson(plugin, message); switch (messageType) { - case UPDATE_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent( + case UPDATE_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent( user -> { plugin.lockPlayer(user.getUuid()); try { @@ -170,16 +170,30 @@ public class RedisManager extends JedisPubSub { } } ); - case REQUEST_USER_DATA -> plugin.getOnlineUser(redisMessage.getTargetUuid()).ifPresent( + case REQUEST_USER_DATA -> redisMessage.getTargetUser(plugin).ifPresent( user -> RedisMessage.create( UUID.fromString(new String(redisMessage.getPayload(), StandardCharsets.UTF_8)), user.createSnapshot(DataSnapshot.SaveCause.INVENTORY_COMMAND).asBytes(plugin) ).dispatch(plugin, RedisMessage.Type.RETURN_USER_DATA) ); + case CHECK_IN_PETITION -> { + if (!redisMessage.isTargetServer(plugin)) { + return; + } + final String payload = new String(redisMessage.getPayload(), StandardCharsets.UTF_8); + final User user = new User(UUID.fromString(payload.split("/")[0]), payload.split("/")[1]); + boolean online = plugin.getDisconnectingPlayers().contains(user.getUuid()) + || plugin.getOnlineUser(user.getUuid()).isEmpty(); + if (!online && !plugin.isLocked(user.getUuid())) { + plugin.debug("[%s] Received check-in petition for online/unlocked user, ignoring".formatted(user.getName())); + return; + } + plugin.getRedisManager().setUserCheckedOut(user, false); + plugin.debug("[%s] Received petition for offline user, checking them in".formatted(user.getName())); + } case RETURN_USER_DATA -> { - final CompletableFuture> future = pendingRequests.get( - redisMessage.getTargetUuid() - ); + final UUID target = redisMessage.getTargetUuid().orElse(null); + final CompletableFuture> future = pendingRequests.get(target); if (future != null) { try { final DataSnapshot.Packed data = DataSnapshot.deserialize(plugin, redisMessage.getPayload()); @@ -188,7 +202,7 @@ public class RedisManager extends JedisPubSub { plugin.log(Level.SEVERE, "An exception occurred returning user data from Redis", e); future.complete(Optional.empty()); } - pendingRequests.remove(redisMessage.getTargetUuid()); + pendingRequests.remove(target); } } } @@ -211,11 +225,17 @@ public class RedisManager extends JedisPubSub { } } + @Blocking public void sendUserDataUpdate(@NotNull User user, @NotNull DataSnapshot.Packed data) { - plugin.runAsync(() -> { - final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin)); - redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA); - }); + final RedisMessage redisMessage = RedisMessage.create(user.getUuid(), data.asBytes(plugin)); + redisMessage.dispatch(plugin, RedisMessage.Type.UPDATE_USER_DATA); + } + + @Blocking + public void petitionServerCheckin(@NotNull String server, @NotNull User user) { + final RedisMessage redisMessage = RedisMessage.create( + server, "%s/%s".formatted(user.getUuid(), user.getName()).getBytes(StandardCharsets.UTF_8)); + redisMessage.dispatch(plugin, RedisMessage.Type.CHECK_IN_PETITION); } public CompletableFuture> getOnlineUserData(@NotNull UUID requestId, @NotNull User user, @@ -421,7 +441,7 @@ public class RedisManager extends JedisPubSub { final long startTime = System.currentTimeMillis(); try (Jedis jedis = jedisPool.getResource()) { jedis.ping(); - return startTime - System.currentTimeMillis(); + return System.currentTimeMillis() - startTime; } } diff --git a/common/src/main/java/net/william278/husksync/redis/RedisMessage.java b/common/src/main/java/net/william278/husksync/redis/RedisMessage.java index cca59694..9c4df6cf 100644 --- a/common/src/main/java/net/william278/husksync/redis/RedisMessage.java +++ b/common/src/main/java/net/william278/husksync/redis/RedisMessage.java @@ -25,25 +25,38 @@ import lombok.Getter; import lombok.Setter; import net.william278.husksync.HuskSync; import net.william278.husksync.adapter.Adaptable; +import net.william278.husksync.user.OnlineUser; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.UUID; +@Setter public class RedisMessage implements Adaptable { + private @Nullable String targetServer; @SerializedName("target_uuid") - private UUID targetUuid; + private @Nullable UUID targetUuid; @Getter @Setter @SerializedName("payload") private byte[] payload; + private RedisMessage(byte[] payload) { + setPayload(payload); + } + private RedisMessage(@NotNull UUID targetUuid, byte[] message) { + this(message); this.setTargetUuid(targetUuid); - this.setPayload(message); + } + + private RedisMessage(@NotNull String targetServer, byte[] message) { + this(message); + this.setTargetServer(targetServer); } @SuppressWarnings("unused") @@ -55,6 +68,11 @@ public class RedisMessage implements Adaptable { return new RedisMessage(targetUuid, message); } + @NotNull + public static RedisMessage create(@NotNull String targetServer, byte[] message) { + return new RedisMessage(targetServer, message); + } + @NotNull public static RedisMessage fromJson(@NotNull HuskSync plugin, @NotNull String json) throws JsonSyntaxException { return plugin.getGson().fromJson(json, RedisMessage.class); @@ -67,20 +85,23 @@ public class RedisMessage implements Adaptable { )); } - @NotNull - public UUID getTargetUuid() { - return targetUuid; + public Optional getTargetUuid() { + return Optional.ofNullable(targetUuid); } - public void setTargetUuid(@NotNull UUID targetUuid) { - this.targetUuid = targetUuid; + public Optional getTargetUser(@NotNull HuskSync plugin) { + return getTargetUuid().flatMap(plugin::getOnlineUser); + } + + public boolean isTargetServer(@NotNull HuskSync plugin) { + return targetServer != null && targetServer.equals(plugin.getServerName()); } public enum Type { - UPDATE_USER_DATA, REQUEST_USER_DATA, - RETURN_USER_DATA; + RETURN_USER_DATA, + CHECK_IN_PETITION; @NotNull public String getMessageChannel(@NotNull String clusterId) { diff --git a/common/src/main/java/net/william278/husksync/sync/DataSyncer.java b/common/src/main/java/net/william278/husksync/sync/DataSyncer.java index 8d4707b3..97509336 100644 --- a/common/src/main/java/net/william278/husksync/sync/DataSyncer.java +++ b/common/src/main/java/net/william278/husksync/sync/DataSyncer.java @@ -185,7 +185,7 @@ public abstract class DataSyncer { final AtomicReference task = new AtomicReference<>(); final AtomicBoolean processing = new AtomicBoolean(false); final Runnable runnable = () -> { - if (user.isOffline()) { + if (user.cannotApplySnapshot()) { task.get().cancel(); return; } diff --git a/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java b/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java index 359d7c04..b24f9cc1 100644 --- a/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java +++ b/common/src/main/java/net/william278/husksync/sync/DelayDataSyncer.java @@ -62,7 +62,10 @@ public class DelayDataSyncer extends DataSyncer { getRedis().setUserServerSwitch(onlineUser); saveData( onlineUser, onlineUser.createSnapshot(DataSnapshot.SaveCause.DISCONNECT), - (user, data) -> getRedis().setUserData(user, data) + (user, data) -> { + getRedis().setUserData(user, data); + plugin.unlockPlayer(user.getUuid()); + } ); }); } diff --git a/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java b/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java index 38f9b978..807c1b51 100644 --- a/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java +++ b/common/src/main/java/net/william278/husksync/sync/LockstepDataSyncer.java @@ -24,6 +24,8 @@ import net.william278.husksync.data.DataSnapshot; import net.william278.husksync.user.OnlineUser; import org.jetbrains.annotations.NotNull; +import java.util.Optional; + public class LockstepDataSyncer extends DataSyncer { public LockstepDataSyncer(@NotNull HuskSync plugin) { @@ -44,13 +46,19 @@ public class LockstepDataSyncer extends DataSyncer { @Override public void syncApplyUserData(@NotNull OnlineUser user) { this.listenForRedisData(user, () -> { - if (user.isOffline()) { - plugin.debug("Not applying data for offline user %s".formatted(user.getName())); + if (user.cannotApplySnapshot()) { + plugin.debug("Not checking data state for user who has gone offline: %s".formatted(user.getName())); return false; } - if (getRedis().getUserCheckedOut(user).isPresent()) { + + // If they are checked out, ask the server to check them back in and return false + final Optional server = getRedis().getUserCheckedOut(user); + if (server.isPresent() && !server.get().equals(plugin.getServerName())) { +// getRedis().petitionServerCheckin(server.get(), user); return false; } + + // If they are checked in - or checked out on *this* server - we can apply their latest data getRedis().setUserCheckedOut(user, true); getRedis().getUserData(user).ifPresentOrElse( data -> user.applySnapshot(data, DataSnapshot.UpdateCause.SYNCHRONIZED), @@ -67,6 +75,7 @@ public class LockstepDataSyncer extends DataSyncer { (user, data) -> { getRedis().setUserData(user, data); getRedis().setUserCheckedOut(user, false); + plugin.unlockPlayer(user.getUuid()); } )); } diff --git a/common/src/main/java/net/william278/husksync/user/OnlineUser.java b/common/src/main/java/net/william278/husksync/user/OnlineUser.java index f0d86645..f2ca4f53 100644 --- a/common/src/main/java/net/william278/husksync/user/OnlineUser.java +++ b/common/src/main/java/net/william278/husksync/user/OnlineUser.java @@ -43,11 +43,27 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo } /** - * Indicates if the player has gone offline + * Indicates if the player is offline * * @return {@code true} if the player has left the server; {@code false} otherwise + * @deprecated use {@code hasDisconnected} instead */ - public abstract boolean isOffline(); + @Deprecated(since = "3.8") + public boolean isOffline() { + return hasDisconnected(); + } + + public abstract boolean hasDisconnected(); + + // Users cannot have snapshots applied if they have disconnected! + @Override + public boolean cannotApplySnapshot() { + if (hasDisconnected()) { + getPlugin().debug("[%s] Cannot apply snapshot as user is offline!".formatted(getName())); + return true; + } + return false; + } @NotNull @Override @@ -117,7 +133,7 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo /** - * Set a player's status from a {@link DataSnapshot} + * Apply a {@link DataSnapshot} to a player, updating their data * * @param snapshot The {@link DataSnapshot} to set the player's status from * @param cause The {@link DataSnapshot.UpdateCause} of the snapshot @@ -125,14 +141,12 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo */ public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) { getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> { - if (!isOffline()) { - getPlugin().debug(String.format("Applying snapshot (%s) to %s (cause: %s)", - snapshot.getShortId(), getName(), cause.getDisplayName() - )); - UserDataHolder.super.applySnapshot( - event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin()) - ); - } + getPlugin().debug(String.format("Attempting to apply snapshot (%s) to %s (cause: %s)", + snapshot.getShortId(), getName(), cause.getDisplayName() + )); + UserDataHolder.super.applySnapshot( + event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin()) + ); }); } diff --git a/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java b/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java index ea973633..ac8f7655 100644 --- a/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java +++ b/fabric/src/main/java/net/william278/husksync/FabricHuskSync.java @@ -54,7 +54,6 @@ import net.william278.husksync.database.PostgresDatabase; import net.william278.husksync.event.FabricEventDispatcher; import net.william278.husksync.event.ModLoadedCallback; import net.william278.husksync.hook.PlanHook; -import net.william278.husksync.listener.EventListener; import net.william278.husksync.listener.FabricEventListener; import net.william278.husksync.listener.LockedHandler; import net.william278.husksync.migrator.Migrator; @@ -109,6 +108,7 @@ public class FabricHuskSync implements DedicatedServerModInitializer, HuskSync, private final Map permissions = Maps.newHashMap(); private final List availableMigrators = Lists.newArrayList(); private final Set lockedPlayers = Sets.newConcurrentHashSet(); + private final Set disconnectingPlayers = Sets.newConcurrentHashSet(); private final Map playerMap = Maps.newConcurrentMap(); private Logger logger; diff --git a/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java b/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java index 101ecb0e..45914f57 100644 --- a/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java +++ b/fabric/src/main/java/net/william278/husksync/data/FabricUserDataHolder.java @@ -34,29 +34,31 @@ public interface FabricUserDataHolder extends UserDataHolder { @Override default Optional getData(@NotNull Identifier id) { - if (!id.isCustom()) { - try { - return switch (id.getKeyValue()) { - case "inventory" -> getInventory(); - case "ender_chest" -> getEnderChest(); - case "potion_effects" -> getPotionEffects(); - case "advancements" -> getAdvancements(); - case "location" -> getLocation(); - case "statistics" -> getStatistics(); - case "health" -> getHealth(); - case "hunger" -> getHunger(); - case "attributes" -> getAttributes(); - case "experience" -> getExperience(); - case "game_mode" -> getGameMode(); - case "flight_status" -> getFlightStatus(); - case "persistent_data" -> getPersistentData(); - default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id)); - }; - } catch (Throwable e) { - getPlugin().debug("Failed to get data for key: " + id.getKeyValue(), e); - } + if (id.isCustom()) { + return Optional.ofNullable(getCustomDataStore().get(id)); + } + + try { + return switch (id.getKeyValue()) { + case "inventory" -> getInventory(); + case "ender_chest" -> getEnderChest(); + case "potion_effects" -> getPotionEffects(); + case "advancements" -> getAdvancements(); + case "location" -> getLocation(); + case "statistics" -> getStatistics(); + case "health" -> getHealth(); + case "hunger" -> getHunger(); + case "attributes" -> getAttributes(); + case "experience" -> getExperience(); + case "game_mode" -> getGameMode(); + case "flight_status" -> getFlightStatus(); + case "persistent_data" -> getPersistentData(); + default -> throw new IllegalStateException(String.format("Unexpected data type: %s", id)); + }; + } catch (Throwable e) { + getPlugin().debug("Failed to get data for key: " + id.asMinimalString(), e); + return Optional.empty(); } - return Optional.ofNullable(getCustomDataStore().get(id)); } @Override diff --git a/fabric/src/main/java/net/william278/husksync/user/FabricUser.java b/fabric/src/main/java/net/william278/husksync/user/FabricUser.java index 161ebe58..d63e2363 100644 --- a/fabric/src/main/java/net/william278/husksync/user/FabricUser.java +++ b/fabric/src/main/java/net/william278/husksync/user/FabricUser.java @@ -64,8 +64,9 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder { } @Override - public boolean isOffline() { - return player == null || player.isDisconnected(); + public boolean hasDisconnected() { + return getPlugin().getDisconnectingPlayers().contains(getUuid()) + || player == null || player.isDisconnected(); } @NotNull @@ -79,7 +80,7 @@ public class FabricUser extends OnlineUser implements FabricUserDataHolder { public void sendToast(@NotNull MineDown title, @NotNull MineDown description, @NotNull String iconMaterial, @NotNull String backgroundType) { plugin.log(Level.WARNING, "Toast notifications are deprecated. " + - "Please change your notification display slot to CHAT, ACTION_BAR or NONE."); + "Please change your notification display slot to CHAT, ACTION_BAR or NONE."); this.sendActionBar(title); }