diff --git a/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java b/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java index 56be8c18..25676fb6 100644 --- a/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java +++ b/bukkit/src/main/java/net/william278/husksync/util/BukkitLegacyConverter.java @@ -39,6 +39,7 @@ import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder; import java.io.ByteArrayInputStream; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.time.OffsetDateTime; import java.util.*; public class BukkitLegacyConverter extends LegacyConverter { @@ -49,7 +50,8 @@ public class BukkitLegacyConverter extends LegacyConverter { @NotNull @Override - public DataSnapshot.Packed convert(@NotNull byte[] data) throws DataAdapter.AdaptionException { + public DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id, + @NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException { final JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data)); final int version = object.getInt("format_version"); if (version != 3) { @@ -61,6 +63,7 @@ public class BukkitLegacyConverter extends LegacyConverter { // Read legacy data from the JSON object final DataSnapshot.Builder builder = DataSnapshot.builder(plugin) + .id(id).timestamp(timestamp) .saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2) .data(readStatusData(object)); readInventory(object).ifPresent(builder::inventory); diff --git a/common/src/main/java/net/william278/husksync/data/DataSnapshot.java b/common/src/main/java/net/william278/husksync/data/DataSnapshot.java index e1fafb2c..ff073712 100644 --- a/common/src/main/java/net/william278/husksync/data/DataSnapshot.java +++ b/common/src/main/java/net/william278/husksync/data/DataSnapshot.java @@ -96,9 +96,11 @@ public class DataSnapshot { return new Builder(plugin); } + // Deserialize a DataSnapshot downloaded from the database (with an ID & Timestamp from the database) @NotNull @ApiStatus.Internal - public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data) throws IllegalStateException { + public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data, @Nullable UUID id, + @Nullable OffsetDateTime timestamp) throws IllegalStateException { final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class); if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) { throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " + @@ -114,7 +116,11 @@ public class DataSnapshot { } if (snapshot.getFormatVersion() < CURRENT_FORMAT_VERSION) { if (plugin.getLegacyConverter().isPresent()) { - return plugin.getLegacyConverter().get().convert(data); + return plugin.getLegacyConverter().get().convert( + data, + Objects.requireNonNull(id, "Attempted legacy conversion with null UUID!"), + Objects.requireNonNull(timestamp, "Attempted legacy conversion with null timestamp!") + ); } throw new IllegalStateException(String.format( "No legacy converter to convert format version: %s", snapshot.getFormatVersion() @@ -129,6 +135,13 @@ public class DataSnapshot { return snapshot; } + // Deserialize a DataSnapshot from a network message payload (without an ID) + @NotNull + @ApiStatus.Internal + public static DataSnapshot.Packed deserialize(@NotNull HuskSync plugin, byte[] data) throws IllegalStateException { + return deserialize(plugin, data, null, null); + } + /** * Return the ID of the snapshot * @@ -393,6 +406,7 @@ public class DataSnapshot { public static class Builder { private final HuskSync plugin; + private UUID id; private SaveCause saveCause; private boolean pinned; private OffsetDateTime timestamp; @@ -403,6 +417,19 @@ public class DataSnapshot { this.pinned = false; this.data = new HashMap<>(); this.timestamp = OffsetDateTime.now(); + this.id = UUID.randomUUID(); + } + + /** + * Set the {@link UUID unique ID} of the snapshot + * + * @param id The {@link UUID} of the snapshot + * @return The builder + */ + @NotNull + public Builder id(@NotNull UUID id) { + this.id = id; + return this; } /** @@ -661,7 +688,7 @@ public class DataSnapshot { throw new IllegalStateException("Cannot build DataSnapshot without a save cause"); } return new Unpacked( - UUID.randomUUID(), + id, pinned || plugin.getSettings().doAutoPin(saveCause), timestamp, saveCause, diff --git a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java index 412756b8..546f9019 100644 --- a/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java +++ b/common/src/main/java/net/william278/husksync/database/MySqlDatabase.java @@ -217,7 +217,7 @@ public class MySqlDatabase extends Database { public Optional getLatestSnapshot(@NotNull User user) { try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` + SELECT `version_uuid`, `timestamp`, `data` FROM `%user_data_table%` WHERE `player_uuid`=? ORDER BY `timestamp` DESC @@ -225,10 +225,14 @@ public class MySqlDatabase extends Database { statement.setString(1, user.getUuid().toString()); final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { + final UUID versionUuid = UUID.fromString(resultSet.getString("version_uuid")); + final OffsetDateTime timestamp = OffsetDateTime.ofInstant( + resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId() + ); final Blob blob = resultSet.getBlob("data"); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray)); + return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp)); } } } catch (SQLException | DataAdapter.AdaptionException e) { @@ -244,17 +248,21 @@ public class MySqlDatabase extends Database { final List retrievedData = new ArrayList<>(); try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` + SELECT `version_uuid`, `timestamp`, `data` FROM `%user_data_table%` WHERE `player_uuid`=? ORDER BY `timestamp` DESC;"""))) { statement.setString(1, user.getUuid().toString()); final ResultSet resultSet = statement.executeQuery(); while (resultSet.next()) { + final UUID versionUuid = UUID.fromString(resultSet.getString("version_uuid")); + final OffsetDateTime timestamp = OffsetDateTime.ofInstant( + resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId() + ); final Blob blob = resultSet.getBlob("data"); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray)); + retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp)); } return retrievedData; } @@ -269,7 +277,7 @@ public class MySqlDatabase extends Database { public Optional getSnapshot(@NotNull User user, @NotNull UUID versionUuid) { try (Connection connection = getConnection()) { try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" - SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` + SELECT `version_uuid`, `timestamp`, `data` FROM `%user_data_table%` WHERE `player_uuid`=? AND `version_uuid`=? ORDER BY `timestamp` DESC @@ -279,9 +287,12 @@ public class MySqlDatabase extends Database { final ResultSet resultSet = statement.executeQuery(); if (resultSet.next()) { final Blob blob = resultSet.getBlob("data"); + final OffsetDateTime timestamp = OffsetDateTime.ofInstant( + resultSet.getTimestamp("timestamp").toInstant(), TimeZone.getDefault().toZoneId() + ); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); blob.free(); - return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray)); + return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp)); } } } catch (SQLException | DataAdapter.AdaptionException e) { diff --git a/common/src/main/java/net/william278/husksync/util/LegacyConverter.java b/common/src/main/java/net/william278/husksync/util/LegacyConverter.java index 9702a0bb..eb4e895b 100644 --- a/common/src/main/java/net/william278/husksync/util/LegacyConverter.java +++ b/common/src/main/java/net/william278/husksync/util/LegacyConverter.java @@ -24,6 +24,9 @@ import net.william278.husksync.adapter.DataAdapter; import net.william278.husksync.data.DataSnapshot; import org.jetbrains.annotations.NotNull; +import java.time.OffsetDateTime; +import java.util.UUID; + public abstract class LegacyConverter { protected final HuskSync plugin; @@ -33,6 +36,7 @@ public abstract class LegacyConverter { } @NotNull - public abstract DataSnapshot.Packed convert(@NotNull byte[] data) throws DataAdapter.AdaptionException; + public abstract DataSnapshot.Packed convert(@NotNull byte[] data, @NotNull UUID id, + @NotNull OffsetDateTime timestamp) throws DataAdapter.AdaptionException; } diff --git a/gradle.properties b/gradle.properties index adf307dd..02221e6c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=16 -plugin_version=3.0 +plugin_version=3.0.1 plugin_archive=husksync plugin_description=A modern, cross-server player data synchronization system