9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-27 02:29:10 +00:00

Compare commits

...

12 Commits
3.0 ... 3.0.2

Author SHA1 Message Date
William
c406f40898 Bump to 3.0.2 2023-09-23 23:34:46 +01:00
William
7561762c25 Fix relocation of com.fatboyindustrial lib 2023-09-23 22:27:12 +01:00
William
d245245083 Fix #get call when appling locked map data, Fix #169 2023-09-23 18:45:06 +01:00
William
2b55e129b3 Slightly improve BukkitData.Items#setContents method 2023-09-23 15:15:10 +01:00
William
0caec74436 Improve stat map resilience for modded block types 2023-09-23 14:08:53 +01:00
William
55e443cd49 Improve error handling on data sync 2023-09-22 22:07:31 +01:00
William
b63e1bd283 Fixup adapting health when scaling 2023-09-22 21:48:39 +01:00
William
575122e6dd Tweak max health syncing calculation, add config option 2023-09-22 21:47:05 +01:00
William
856cbb9caa Allow conversion of v1-v3 data snapshots 2023-09-22 21:27:11 +01:00
William
7034a97d3a Fix wrong timestamp/UUID being used for legacy conversion (#167)
* Maintain legacy snapshot IDs when updating

* Also maintain timestamps during conversion

* Actually implement timestamp fix in LegacyConverter
2023-09-22 16:12:08 +01:00
William
635edb930f Fix wrong syntax message on /userdata restore, Close #166 2023-09-22 13:26:31 +01:00
dependabot[bot]
1d3e4b7a20 Bump de.tr7zw:item-nbt-api from 2.12.0-SNAPSHOT to 2.12.0 (#165)
Bumps de.tr7zw:item-nbt-api from 2.12.0-SNAPSHOT to 2.12.0.

---
updated-dependencies:
- dependency-name: de.tr7zw:item-nbt-api
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-09-22 09:45:40 +01:00
13 changed files with 175 additions and 92 deletions

View File

@@ -10,7 +10,7 @@ dependencies {
implementation 'net.kyori:adventure-platform-bukkit:4.3.0'
implementation 'dev.triumphteam:triumph-gui:3.1.5'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3'
implementation 'de.tr7zw:item-nbt-api:2.12.0-SNAPSHOT'
implementation 'de.tr7zw:item-nbt-api:2.12.0'
compileOnly 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'commons-io:commons-io:2.13.0'
@@ -34,7 +34,7 @@ shadowJar {
relocate 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
relocate 'org.json', 'net.william278.husksync.libraries.json'
relocate 'com.fatboyindustrial', 'net.william278.husktowns.libraries'
relocate 'com.fatboyindustrial', 'net.william278.husksync.libraries'
relocate 'de.themoep', 'net.william278.husksync.libraries'
relocate 'net.kyori', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', 'net.william278.husksync.libraries'

View File

@@ -28,10 +28,7 @@ import net.william278.husksync.HuskSync;
import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.user.BukkitUser;
import org.apache.commons.lang.NotImplementedException;
import org.bukkit.Bukkit;
import org.bukkit.GameRule;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.*;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
@@ -53,14 +50,8 @@ import java.util.stream.Collectors;
public abstract class BukkitData implements Data {
@Override
public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) {
final BukkitUser user = (BukkitUser) dataHolder;
try {
this.apply(user, (BukkitHuskSync) plugin);
} catch (Throwable e) {
plugin.log(Level.WARNING, String.format("[%s] Failed to apply %s data object; skipping",
user.getUsername(), this.getClass().getSimpleName()), e);
}
public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) throws IllegalStateException {
this.apply((BukkitUser) dataHolder, (BukkitHuskSync) plugin);
}
public abstract void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException;
@@ -104,14 +95,9 @@ public abstract class BukkitData implements Data {
@Override
public void setContents(@NotNull Data.Items contents) {
System.arraycopy(
((BukkitData.Items) contents).getContents(),
0, this.contents,
0, this.contents.length
);
this.setContents(((BukkitData.Items) contents).getContents());
}
@SuppressWarnings("unused")
public void setContents(@NotNull ItemStack[] contents) {
System.arraycopy(contents, 0, this.contents, 0, this.contents.length);
}
@@ -535,7 +521,7 @@ public abstract class BukkitData implements Data {
}
public static class Statistics extends BukkitData implements Data.Statistics {
private Map<Statistic, Integer> untypedStatistics;
private Map<Statistic, Integer> genericStatistics;
private Map<Statistic, Map<Material, Integer>> blockStatistics;
private Map<Statistic, Map<Material, Integer>> itemStatistics;
private Map<Statistic, Map<EntityType, Integer>> entityStatistics;
@@ -544,7 +530,7 @@ public abstract class BukkitData implements Data {
@NotNull Map<Statistic, Map<Material, Integer>> blockStatistics,
@NotNull Map<Statistic, Map<Material, Integer>> itemStatistics,
@NotNull Map<Statistic, Map<EntityType, Integer>> entityStatistics) {
this.untypedStatistics = genericStatistics;
this.genericStatistics = genericStatistics;
this.blockStatistics = blockStatistics;
this.itemStatistics = itemStatistics;
this.entityStatistics = entityStatistics;
@@ -665,7 +651,7 @@ public abstract class BukkitData implements Data {
@Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
untypedStatistics.forEach((stat, value) -> applyStat(user, stat, null, value));
genericStatistics.forEach((stat, value) -> applyStat(user, stat, null, value));
blockStatistics.forEach((stat, m) -> m.forEach((block, value) -> applyStat(user, stat, block, value)));
itemStatistics.forEach((stat, m) -> m.forEach((item, value) -> applyStat(user, stat, item, value)));
entityStatistics.forEach((stat, m) -> m.forEach((entity, value) -> applyStat(user, stat, entity, value)));
@@ -688,45 +674,41 @@ public abstract class BukkitData implements Data {
@NotNull
@Override
public Map<String, Integer> getGenericStatistics() {
return untypedStatistics.entrySet().stream().collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue()), TreeMap::putAll
);
return convertStatistics(genericStatistics);
}
@NotNull
@Override
public Map<String, Map<String, Integer>> getBlockStatistics() {
return blockStatistics.entrySet().stream().collect(
return blockStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect(
TreeMap::new,
(m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
)), TreeMap::putAll
(m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
);
}
@NotNull
@Override
public Map<String, Map<String, Integer>> getItemStatistics() {
return itemStatistics.entrySet().stream().collect(
return itemStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect(
TreeMap::new,
(m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
)), TreeMap::putAll
(m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
);
}
@NotNull
@Override
public Map<String, Map<String, Integer>> getEntityStatistics() {
return entityStatistics.entrySet().stream().collect(
return entityStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect(
TreeMap::new,
(m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
)), TreeMap::putAll
(m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
);
}
@NotNull
private <T extends Keyed> Map<String, Integer> convertStatistics(@NotNull Map<T, Integer> stats) {
return stats.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue()), TreeMap::putAll
);
}
@@ -808,11 +790,11 @@ public abstract class BukkitData implements Data {
@NotNull
public static BukkitData.Health adapt(@NotNull Player player) {
final double maxHealth = getMaxHealth(player);
return from(
player.getHealth(),
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH),
"Missing max health attribute").getValue(),
player.getHealthScale()
Math.min(player.getHealth(), maxHealth),
maxHealth,
player.isHealthScaled() ? player.getHealthScale() : 0d
);
}
@@ -822,9 +804,10 @@ public abstract class BukkitData implements Data {
// Set base max health
final AttributeInstance maxHealthAttribute = Objects.requireNonNull(
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Missing max health attribute");
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null"
);
double currentMaxHealth = maxHealthAttribute.getBaseValue();
if (maxHealth != 0d) {
if (plugin.getSettings().doSynchronizeMaxHealth() && maxHealth != 0d) {
maxHealthAttribute.setBaseValue(maxHealth);
currentMaxHealth = maxHealth;
}
@@ -853,6 +836,30 @@ public abstract class BukkitData implements Data {
}
}
/**
* Returns a {@link Player}'s maximum health, minus any health boost effects
*
* @param player The {@link Player} to get the maximum health of
* @return The {@link Player}'s max health
*/
private static double getMaxHealth(@NotNull Player player) {
double maxHealth = Objects.requireNonNull(
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null"
).getBaseValue();
// If the player has additional health bonuses from synchronized potion effects,
// subtract these from this number as they are synchronized separately
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20d) {
final PotionEffect healthBoost = Objects.requireNonNull(
player.getPotionEffect(PotionEffectType.HEALTH_BOOST), "Health boost effect was null"
);
final double boostEffect = 4 * (healthBoost.getAmplifier() + 1);
maxHealth -= boostEffect;
}
return maxHealth;
}
@Override
public double getHealth() {
return health;

View File

@@ -39,7 +39,9 @@ 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.*;
import java.util.logging.Level;
public class BukkitLegacyConverter extends LegacyConverter {
@@ -49,18 +51,17 @@ 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) {
throw new DataAdapter.AdaptionException(String.format(
"Unsupported legacy data format version: %s. Please downgrade to an earlier version of HuskSync, " +
"perform a manual legacy migration, then attempt to upgrade again.", version
));
plugin.log(Level.WARNING, String.format("Converting data from older v2 data format (%s).", version));
}
// 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);

View File

@@ -133,12 +133,15 @@ public interface BukkitMapPersister {
final MapMeta meta = Objects.requireNonNull((MapMeta) map.getItemMeta());
NBT.get(map, nbt -> {
if (!nbt.hasTag(MAP_DATA_KEY)) {
return nbt;
return;
}
final ReadableNBT mapData = nbt.getCompound(MAP_DATA_KEY);
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
if (mapData == null || mapIds == null) {
return;
}
// Search for an existing map view
final ReadableNBT mapIds = nbt.getCompound(MAP_VIEW_ID_MAPPINGS_KEY);
Optional<String> world = Optional.empty();
for (String worldUid : mapIds.getKeys()) {
world = Bukkit.getWorlds().stream()
@@ -157,7 +160,7 @@ public interface BukkitMapPersister {
meta.setMapView(view);
map.setItemMeta(meta);
getPlugin().debug(String.format("View exists (#%s); updated map (UID: %s)", view.getId(), uid));
return nbt;
return;
}
}
@@ -165,10 +168,11 @@ public interface BukkitMapPersister {
final MapData canvasData;
try {
getPlugin().debug("Deserializing map data from NBT and generating view...");
canvasData = MapData.fromByteArray(mapData.getByteArray(MAP_PIXEL_DATA_KEY));
canvasData = MapData.fromByteArray(Objects.requireNonNull(mapData.getByteArray(MAP_PIXEL_DATA_KEY),
"Map pixel data is null"));
} catch (Throwable e) {
getPlugin().log(Level.WARNING, "Failed to deserialize map data from NBT", e);
return nbt;
return;
}
// Add a renderer to the map with the data
@@ -179,10 +183,11 @@ public interface BukkitMapPersister {
// Set the map view ID in NBT
NBT.modify(map, editable -> {
editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY).setInteger(worldUid, view.getId());
Objects.requireNonNull(editable.getCompound(MAP_VIEW_ID_MAPPINGS_KEY),
"Map view ID mappings compound is null")
.setInteger(worldUid, view.getId());
});
getPlugin().debug(String.format("Generated view (#%s) and updated map (UID: %s)", view.getId(), worldUid));
return nbt;
});
return map;
}

View File

@@ -129,7 +129,7 @@ public class UserDataCommand extends Command implements TabProvider {
case "restore" -> {
if (optionalUuid.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>")
"/userdata restore <username> <version_uuid>")
.ifPresent(executor::sendMessage);
return;
}

View File

@@ -177,7 +177,11 @@ public class Settings {
@YamlComment("(Experimental) Persist Cartography Table locked maps to let them be viewed on any server")
@YamlKey("synchronization.persist_locked_maps")
private boolean persistLockedMaps = false;
private boolean persistLockedMaps = true;
@YamlComment("Whether to synchronize player max health (requires health syncing to be enabled)")
@YamlKey("synchronization.synchronize_max_health")
private boolean synchronizeMaxHealth = true;
@YamlComment("Whether dead players who log out and log in to a different server should have their items saved. "
+ "You may need to modify this if you're using the keepInventory gamerule.")
@@ -352,6 +356,10 @@ public class Settings {
return synchronizeDeadPlayersChangingServer;
}
public boolean doSynchronizeMaxHealth() {
return synchronizeMaxHealth;
}
public int getNetworkLatencyMilliseconds() {
return networkLatencyMilliseconds;
}

View File

@@ -45,7 +45,7 @@ public class DataSnapshot {
/*
* Current version of the snapshot data format.
* HuskSync v3.0 uses v4; HuskSync v2.0 uses v3. HuskSync v1.0 uses v1 or v2
* HuskSync v3.0 uses v4; HuskSync v2.0 uses v1-v3
*/
protected static final int CURRENT_FORMAT_VERSION = 4;
@@ -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
*
@@ -227,12 +240,6 @@ public class DataSnapshot {
/**
* Get the format version of the snapshot (indicating the version of HuskSync that created it)
* <ul>
* <li>1: HuskSync v1.0+</li>
* <li>2: HuskSync v1.5+</li>
* <li>3: HuskSync v2.0+</li>
* <li>4: HuskSync v3.0+</li>
* </ul>
*
* @return The format version of the snapshot
* @since 3.0
@@ -393,6 +400,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 +411,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 +682,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,

View File

@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
/**
* A holder of data in the form of {@link Data}s, which can be synced
@@ -83,22 +84,40 @@ public interface UserDataHolder extends DataHolder {
* The {@code runAfter} callback function will be run after the snapshot has been applied.
*
* @param snapshot the snapshot to apply
* @param runAfter the function to run asynchronously after the snapshot has been applied
* @param runAfter a consumer accepting a boolean value, indicating if the data was successfully applied,
* which will be run after the snapshot has been applied
* @since 3.0
*/
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<UserDataHolder> runAfter) {
default void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull ThrowingConsumer<Boolean> runAfter) {
final HuskSync plugin = getPlugin();
final DataSnapshot.Unpacked unpacked = snapshot.unpack(plugin);
// Unpack the snapshot
final DataSnapshot.Unpacked unpacked;
try {
unpacked = snapshot.unpack(plugin);
} catch (Throwable e) {
plugin.log(Level.SEVERE, String.format("Failed to unpack data snapshot for %s", getUsername()), e);
return;
}
// Synchronously attempt to apply the snapshot
plugin.runSync(() -> {
unpacked.getData().forEach((type, data) -> {
if (plugin.getSettings().isSyncFeatureEnabled(type)) {
if (type.isCustom()) {
getCustomDataStore().put(type, data);
try {
for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
final Identifier identifier = entry.getKey();
if (plugin.getSettings().isSyncFeatureEnabled(identifier)) {
if (identifier.isCustom()) {
getCustomDataStore().put(identifier, entry.getValue());
}
entry.getValue().apply(this, plugin);
}
data.apply(this, plugin);
}
});
plugin.runAsync(() -> runAfter.accept(this));
} catch (Throwable e) {
plugin.log(Level.SEVERE, String.format("Failed to apply data snapshot to %s", getUsername()), e);
plugin.runAsync(() -> runAfter.accept(false));
return;
}
plugin.runAsync(() -> runAfter.accept(true));
});
}
@@ -157,6 +176,9 @@ public interface UserDataHolder extends DataHolder {
this.setData(Identifier.PERSISTENT_DATA, persistentData);
}
@NotNull
String getUsername();
@NotNull
Map<Identifier, Data> getCustomDataStore();

View File

@@ -217,7 +217,7 @@ public class MySqlDatabase extends Database {
public Optional<DataSnapshot.Packed> 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<DataSnapshot.Packed> 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<DataSnapshot.Packed> 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) {

View File

@@ -125,12 +125,14 @@ public abstract class OnlineUser extends User implements CommandUser, UserDataHo
* Set a player's status from a {@link DataSnapshot}
*
* @param snapshot The {@link DataSnapshot} to set the player's status from
* @param cause The {@link DataSnapshot.UpdateCause} of the snapshot
* @since 3.0
*/
public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
if (!isOffline()) {
UserDataHolder.super.applySnapshot(
event.getData(), (owner) -> completeSync(true, cause, getPlugin())
event.getData(), (succeeded) -> completeSync(succeeded, cause, getPlugin())
);
}
});

View File

@@ -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;
}

View File

@@ -77,7 +77,9 @@ synchronization:
# Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
notification_display_slot: ACTION_BAR
# (Experimental) Persist Cartography Table locked maps to let them be viewed on any server
persist_locked_maps: false
persist_locked_maps: true
# Whether to synchronize player max health (requires health syncing to be enabled)
synchronize_max_health: true
# Whether dead players who log out and log in to a different server should have their items saved. You may need to modify this if you're using the keepInventory gamerule.
synchronize_dead_players_changing_server: true
# How long, in milliseconds, this server should wait for a response from the redis server before pulling data from the database instead (i.e., if the user did not change servers).

View File

@@ -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.2
plugin_archive=husksync
plugin_description=A modern, cross-server player data synchronization system