9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2025-12-27 18:49:11 +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 'net.kyori:adventure-platform-bukkit:4.3.0'
implementation 'dev.triumphteam:triumph-gui:3.1.5' implementation 'dev.triumphteam:triumph-gui:3.1.5'
implementation 'space.arim.morepaperlib:morepaperlib:0.4.3' 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 'org.spigotmc:spigot-api:1.16.5-R0.1-SNAPSHOT'
compileOnly 'commons-io:commons-io:2.13.0' 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 'org.apache.commons.lang3', 'net.william278.husksync.libraries.commons.lang3'
relocate 'com.google.gson', 'net.william278.husksync.libraries.gson' relocate 'com.google.gson', 'net.william278.husksync.libraries.gson'
relocate 'org.json', 'net.william278.husksync.libraries.json' 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 'de.themoep', 'net.william278.husksync.libraries'
relocate 'net.kyori', 'net.william278.husksync.libraries' relocate 'net.kyori', 'net.william278.husksync.libraries'
relocate 'org.jetbrains', '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.adapter.Adaptable;
import net.william278.husksync.user.BukkitUser; import net.william278.husksync.user.BukkitUser;
import org.apache.commons.lang.NotImplementedException; import org.apache.commons.lang.NotImplementedException;
import org.bukkit.Bukkit; import org.bukkit.*;
import org.bukkit.GameRule;
import org.bukkit.Material;
import org.bukkit.Statistic;
import org.bukkit.advancement.AdvancementProgress; import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute; import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeInstance;
@@ -53,14 +50,8 @@ import java.util.stream.Collectors;
public abstract class BukkitData implements Data { public abstract class BukkitData implements Data {
@Override @Override
public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) { public final void apply(@NotNull UserDataHolder dataHolder, @NotNull HuskSync plugin) throws IllegalStateException {
final BukkitUser user = (BukkitUser) dataHolder; this.apply((BukkitUser) dataHolder, (BukkitHuskSync) plugin);
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 abstract void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException; public abstract void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException;
@@ -104,14 +95,9 @@ public abstract class BukkitData implements Data {
@Override @Override
public void setContents(@NotNull Data.Items contents) { public void setContents(@NotNull Data.Items contents) {
System.arraycopy( this.setContents(((BukkitData.Items) contents).getContents());
((BukkitData.Items) contents).getContents(),
0, this.contents,
0, this.contents.length
);
} }
@SuppressWarnings("unused")
public void setContents(@NotNull ItemStack[] contents) { public void setContents(@NotNull ItemStack[] contents) {
System.arraycopy(contents, 0, this.contents, 0, this.contents.length); 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 { 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>> blockStatistics;
private Map<Statistic, Map<Material, Integer>> itemStatistics; private Map<Statistic, Map<Material, Integer>> itemStatistics;
private Map<Statistic, Map<EntityType, Integer>> entityStatistics; 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>> blockStatistics,
@NotNull Map<Statistic, Map<Material, Integer>> itemStatistics, @NotNull Map<Statistic, Map<Material, Integer>> itemStatistics,
@NotNull Map<Statistic, Map<EntityType, Integer>> entityStatistics) { @NotNull Map<Statistic, Map<EntityType, Integer>> entityStatistics) {
this.untypedStatistics = genericStatistics; this.genericStatistics = genericStatistics;
this.blockStatistics = blockStatistics; this.blockStatistics = blockStatistics;
this.itemStatistics = itemStatistics; this.itemStatistics = itemStatistics;
this.entityStatistics = entityStatistics; this.entityStatistics = entityStatistics;
@@ -665,7 +651,7 @@ public abstract class BukkitData implements Data {
@Override @Override
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException { 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))); 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))); 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))); entityStatistics.forEach((stat, m) -> m.forEach((entity, value) -> applyStat(user, stat, entity, value)));
@@ -688,45 +674,41 @@ public abstract class BukkitData implements Data {
@NotNull @NotNull
@Override @Override
public Map<String, Integer> getGenericStatistics() { public Map<String, Integer> getGenericStatistics() {
return untypedStatistics.entrySet().stream().collect( return convertStatistics(genericStatistics);
TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue()), TreeMap::putAll
);
} }
@NotNull @NotNull
@Override @Override
public Map<String, Map<String, Integer>> getBlockStatistics() { public Map<String, Map<String, Integer>> getBlockStatistics() {
return blockStatistics.entrySet().stream().collect( return blockStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new, TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect( (m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
TreeMap::new,
(m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
)), TreeMap::putAll
); );
} }
@NotNull @NotNull
@Override @Override
public Map<String, Map<String, Integer>> getItemStatistics() { public Map<String, Map<String, Integer>> getItemStatistics() {
return itemStatistics.entrySet().stream().collect( return itemStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new, TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect( (m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
TreeMap::new,
(m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll
)), TreeMap::putAll
); );
} }
@NotNull @NotNull
@Override @Override
public Map<String, Map<String, Integer>> getEntityStatistics() { public Map<String, Map<String, Integer>> getEntityStatistics() {
return entityStatistics.entrySet().stream().collect( return entityStatistics.entrySet().stream().filter(entry -> entry.getKey() != null).collect(
TreeMap::new, TreeMap::new,
(m, e) -> m.put(e.getKey().getKey().toString(), e.getValue().entrySet().stream().collect( (m, e) -> m.put(e.getKey().getKey().toString(), convertStatistics(e.getValue())), TreeMap::putAll
TreeMap::new, );
(m2, e2) -> m2.put(e2.getKey().getKey().toString(), e2.getValue()), TreeMap::putAll }
)), 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 @NotNull
public static BukkitData.Health adapt(@NotNull Player player) { public static BukkitData.Health adapt(@NotNull Player player) {
final double maxHealth = getMaxHealth(player);
return from( return from(
player.getHealth(), Math.min(player.getHealth(), maxHealth),
Objects.requireNonNull(player.getAttribute(Attribute.GENERIC_MAX_HEALTH), maxHealth,
"Missing max health attribute").getValue(), player.isHealthScaled() ? player.getHealthScale() : 0d
player.getHealthScale()
); );
} }
@@ -822,9 +804,10 @@ public abstract class BukkitData implements Data {
// Set base max health // Set base max health
final AttributeInstance maxHealthAttribute = Objects.requireNonNull( 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(); double currentMaxHealth = maxHealthAttribute.getBaseValue();
if (maxHealth != 0d) { if (plugin.getSettings().doSynchronizeMaxHealth() && maxHealth != 0d) {
maxHealthAttribute.setBaseValue(maxHealth); maxHealthAttribute.setBaseValue(maxHealth);
currentMaxHealth = 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 @Override
public double getHealth() { public double getHealth() {
return health; return health;

View File

@@ -39,7 +39,9 @@ import org.yaml.snakeyaml.external.biz.base64Coder.Base64Coder;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.time.OffsetDateTime;
import java.util.*; import java.util.*;
import java.util.logging.Level;
public class BukkitLegacyConverter extends LegacyConverter { public class BukkitLegacyConverter extends LegacyConverter {
@@ -49,18 +51,17 @@ public class BukkitLegacyConverter extends LegacyConverter {
@NotNull @NotNull
@Override @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 JSONObject object = new JSONObject(plugin.getDataAdapter().bytesToString(data));
final int version = object.getInt("format_version"); final int version = object.getInt("format_version");
if (version != 3) { if (version != 3) {
throw new DataAdapter.AdaptionException(String.format( plugin.log(Level.WARNING, String.format("Converting data from older v2 data format (%s).", version));
"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
));
} }
// Read legacy data from the JSON object // Read legacy data from the JSON object
final DataSnapshot.Builder builder = DataSnapshot.builder(plugin) final DataSnapshot.Builder builder = DataSnapshot.builder(plugin)
.id(id).timestamp(timestamp)
.saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2) .saveCause(DataSnapshot.SaveCause.CONVERTED_FROM_V2)
.data(readStatusData(object)); .data(readStatusData(object));
readInventory(object).ifPresent(builder::inventory); readInventory(object).ifPresent(builder::inventory);

View File

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

View File

@@ -129,7 +129,7 @@ public class UserDataCommand extends Command implements TabProvider {
case "restore" -> { case "restore" -> {
if (optionalUuid.isEmpty()) { if (optionalUuid.isEmpty()) {
plugin.getLocales().getLocale("error_invalid_syntax", plugin.getLocales().getLocale("error_invalid_syntax",
"/userdata delete <username> <version_uuid>") "/userdata restore <username> <version_uuid>")
.ifPresent(executor::sendMessage); .ifPresent(executor::sendMessage);
return; 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") @YamlComment("(Experimental) Persist Cartography Table locked maps to let them be viewed on any server")
@YamlKey("synchronization.persist_locked_maps") @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. " @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.") + "You may need to modify this if you're using the keepInventory gamerule.")
@@ -352,6 +356,10 @@ public class Settings {
return synchronizeDeadPlayersChangingServer; return synchronizeDeadPlayersChangingServer;
} }
public boolean doSynchronizeMaxHealth() {
return synchronizeMaxHealth;
}
public int getNetworkLatencyMilliseconds() { public int getNetworkLatencyMilliseconds() {
return networkLatencyMilliseconds; return networkLatencyMilliseconds;
} }

View File

@@ -45,7 +45,7 @@ public class DataSnapshot {
/* /*
* Current version of the snapshot data format. * 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; protected static final int CURRENT_FORMAT_VERSION = 4;
@@ -96,9 +96,11 @@ public class DataSnapshot {
return new Builder(plugin); return new Builder(plugin);
} }
// Deserialize a DataSnapshot downloaded from the database (with an ID & Timestamp from the database)
@NotNull @NotNull
@ApiStatus.Internal @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); final DataSnapshot.Packed snapshot = plugin.getDataAdapter().fromBytes(data, DataSnapshot.Packed.class);
if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) { if (snapshot.getMinecraftVersion().compareTo(plugin.getMinecraftVersion()) > 0) {
throw new IllegalStateException(String.format("Cannot set data for user because the Minecraft version of " + 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 (snapshot.getFormatVersion() < CURRENT_FORMAT_VERSION) {
if (plugin.getLegacyConverter().isPresent()) { 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( throw new IllegalStateException(String.format(
"No legacy converter to convert format version: %s", snapshot.getFormatVersion() "No legacy converter to convert format version: %s", snapshot.getFormatVersion()
@@ -129,6 +135,13 @@ public class DataSnapshot {
return snapshot; 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 * 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) * 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 * @return The format version of the snapshot
* @since 3.0 * @since 3.0
@@ -393,6 +400,7 @@ public class DataSnapshot {
public static class Builder { public static class Builder {
private final HuskSync plugin; private final HuskSync plugin;
private UUID id;
private SaveCause saveCause; private SaveCause saveCause;
private boolean pinned; private boolean pinned;
private OffsetDateTime timestamp; private OffsetDateTime timestamp;
@@ -403,6 +411,19 @@ public class DataSnapshot {
this.pinned = false; this.pinned = false;
this.data = new HashMap<>(); this.data = new HashMap<>();
this.timestamp = OffsetDateTime.now(); 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"); throw new IllegalStateException("Cannot build DataSnapshot without a save cause");
} }
return new Unpacked( return new Unpacked(
UUID.randomUUID(), id,
pinned || plugin.getSettings().doAutoPin(saveCause), pinned || plugin.getSettings().doAutoPin(saveCause),
timestamp, timestamp,
saveCause, saveCause,

View File

@@ -26,6 +26,7 @@ import org.jetbrains.annotations.NotNull;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.logging.Level;
/** /**
* A holder of data in the form of {@link Data}s, which can be synced * 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. * The {@code runAfter} callback function will be run after the snapshot has been applied.
* *
* @param snapshot the snapshot to apply * @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 * @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 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(() -> { plugin.runSync(() -> {
unpacked.getData().forEach((type, data) -> { try {
if (plugin.getSettings().isSyncFeatureEnabled(type)) { for (Map.Entry<Identifier, Data> entry : unpacked.getData().entrySet()) {
if (type.isCustom()) { final Identifier identifier = entry.getKey();
getCustomDataStore().put(type, data); if (plugin.getSettings().isSyncFeatureEnabled(identifier)) {
if (identifier.isCustom()) {
getCustomDataStore().put(identifier, entry.getValue());
}
entry.getValue().apply(this, plugin);
} }
data.apply(this, plugin);
} }
}); } catch (Throwable e) {
plugin.runAsync(() -> runAfter.accept(this)); 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); this.setData(Identifier.PERSISTENT_DATA, persistentData);
} }
@NotNull
String getUsername();
@NotNull @NotNull
Map<Identifier, Data> getCustomDataStore(); Map<Identifier, Data> getCustomDataStore();

View File

@@ -217,7 +217,7 @@ public class MySqlDatabase extends Database {
public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) { public Optional<DataSnapshot.Packed> getLatestSnapshot(@NotNull User user) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` SELECT `version_uuid`, `timestamp`, `data`
FROM `%user_data_table%` FROM `%user_data_table%`
WHERE `player_uuid`=? WHERE `player_uuid`=?
ORDER BY `timestamp` DESC ORDER BY `timestamp` DESC
@@ -225,10 +225,14 @@ public class MySqlDatabase extends Database {
statement.setString(1, user.getUuid().toString()); statement.setString(1, user.getUuid().toString());
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) { 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 Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free(); blob.free();
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray)); return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
} }
} }
} catch (SQLException | DataAdapter.AdaptionException e) { } catch (SQLException | DataAdapter.AdaptionException e) {
@@ -244,17 +248,21 @@ public class MySqlDatabase extends Database {
final List<DataSnapshot.Packed> retrievedData = new ArrayList<>(); final List<DataSnapshot.Packed> retrievedData = new ArrayList<>();
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` SELECT `version_uuid`, `timestamp`, `data`
FROM `%user_data_table%` FROM `%user_data_table%`
WHERE `player_uuid`=? WHERE `player_uuid`=?
ORDER BY `timestamp` DESC;"""))) { ORDER BY `timestamp` DESC;"""))) {
statement.setString(1, user.getUuid().toString()); statement.setString(1, user.getUuid().toString());
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) { 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 Blob blob = resultSet.getBlob("data");
final byte[] dataByteArray = blob.getBytes(1, (int) blob.length()); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free(); blob.free();
retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray)); retrievedData.add(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
} }
return retrievedData; return retrievedData;
} }
@@ -269,7 +277,7 @@ public class MySqlDatabase extends Database {
public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) { public Optional<DataSnapshot.Packed> getSnapshot(@NotNull User user, @NotNull UUID versionUuid) {
try (Connection connection = getConnection()) { try (Connection connection = getConnection()) {
try (PreparedStatement statement = connection.prepareStatement(formatStatementTables(""" try (PreparedStatement statement = connection.prepareStatement(formatStatementTables("""
SELECT `version_uuid`, `timestamp`, `save_cause`, `pinned`, `data` SELECT `version_uuid`, `timestamp`, `data`
FROM `%user_data_table%` FROM `%user_data_table%`
WHERE `player_uuid`=? AND `version_uuid`=? WHERE `player_uuid`=? AND `version_uuid`=?
ORDER BY `timestamp` DESC ORDER BY `timestamp` DESC
@@ -279,9 +287,12 @@ public class MySqlDatabase extends Database {
final ResultSet resultSet = statement.executeQuery(); final ResultSet resultSet = statement.executeQuery();
if (resultSet.next()) { if (resultSet.next()) {
final Blob blob = resultSet.getBlob("data"); 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()); final byte[] dataByteArray = blob.getBytes(1, (int) blob.length());
blob.free(); blob.free();
return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray)); return Optional.of(DataSnapshot.deserialize(plugin, dataByteArray, versionUuid, timestamp));
} }
} }
} catch (SQLException | DataAdapter.AdaptionException e) { } 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} * Set a player's status from a {@link DataSnapshot}
* *
* @param snapshot The {@link DataSnapshot} to set the player's status from * @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) { public void applySnapshot(@NotNull DataSnapshot.Packed snapshot, @NotNull DataSnapshot.UpdateCause cause) {
getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> { getPlugin().fireEvent(getPlugin().getPreSyncEvent(this, snapshot), (event) -> {
if (!isOffline()) { if (!isOffline()) {
UserDataHolder.super.applySnapshot( 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 net.william278.husksync.data.DataSnapshot;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.time.OffsetDateTime;
import java.util.UUID;
public abstract class LegacyConverter { public abstract class LegacyConverter {
protected final HuskSync plugin; protected final HuskSync plugin;
@@ -33,6 +36,7 @@ public abstract class LegacyConverter {
} }
@NotNull @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) # Where to display sync notifications (ACTION_BAR, CHAT, TOAST or NONE)
notification_display_slot: ACTION_BAR notification_display_slot: ACTION_BAR
# (Experimental) Persist Cartography Table locked maps to let them be viewed on any server # (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. # 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 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). # 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 org.gradle.daemon=true
javaVersion=16 javaVersion=16
plugin_version=3.0 plugin_version=3.0.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