refactor: add serialization identifier dependencies for applying data (#309)

* refactor: add serialization identifier dependencies for applying data

* fix: correct issues with deterministic sync order

* refactor: adjust base data type dependencies

* refactor: cleanup imports/trim whitespace

* docs: Document Identifier dependencies

* feat: fix issues with health scaling
feat/data-edit-commands
William 6 months ago committed by GitHub
parent c4adec3082
commit e0b81e4c76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -85,7 +85,9 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
private static final int METRICS_ID = 13140; private static final int METRICS_ID = 13140;
private static final String PLATFORM_TYPE_ID = "bukkit"; private static final String PLATFORM_TYPE_ID = "bukkit";
private final Map<Identifier, Serializer<? extends Data>> serializers = Maps.newLinkedHashMap(); private final TreeMap<Identifier, Serializer<? extends Data>> serializers = Maps.newTreeMap(
SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR
);
private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap(); private final Map<UUID, Map<Identifier, Data>> playerCustomDataStore = Maps.newConcurrentMap();
private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap(); private final Map<Integer, MapView> mapViews = Maps.newConcurrentMap();
private final List<Migrator> availableMigrators = Lists.newArrayList(); private final List<Migrator> availableMigrators = Lists.newArrayList();
@ -143,19 +145,20 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
// Prepare serializers // Prepare serializers
initialize("data serializers", (plugin) -> { initialize("data serializers", (plugin) -> {
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this));
registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this)); registerSerializer(Identifier.INVENTORY, new BukkitSerializer.Inventory(this));
registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this)); registerSerializer(Identifier.ENDER_CHEST, new BukkitSerializer.EnderChest(this));
registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this)); registerSerializer(Identifier.ADVANCEMENTS, new BukkitSerializer.Advancements(this));
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class)); registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class));
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class)); registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class)); registerSerializer(Identifier.GAME_MODE, new BukkitSerializer.Json<>(this, BukkitData.GameMode.class));
registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class)); registerSerializer(Identifier.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this)); registerSerializer(Identifier.ATTRIBUTES, new BukkitSerializer.Json<>(this, BukkitData.Attributes.class));
registerSerializer(Identifier.STATISTICS, new BukkitSerializer.Json<>(this, BukkitData.Statistics.class)); registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
registerSerializer(Identifier.HUNGER, new BukkitSerializer.Json<>(this, BukkitData.Hunger.class));
registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class)); registerSerializer(Identifier.EXPERIENCE, new BukkitSerializer.Json<>(this, BukkitData.Experience.class));
registerSerializer(Identifier.PERSISTENT_DATA, new BukkitSerializer.PersistentData(this)); registerSerializer(Identifier.LOCATION, new BukkitSerializer.Json<>(this, BukkitData.Location.class));
validateDependencies();
}); });
// Setup available migrators // Setup available migrators
@ -289,7 +292,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
try { try {
new Metrics(this, metricsId); new Metrics(this, metricsId);
} catch (Throwable e) { } catch (Throwable e) {
log(Level.WARNING, "Failed to register bStats metrics (" + e.getMessage() + ")"); log(Level.WARNING, "Failed to register bStats metrics (%s)".formatted(e.getMessage()));
} }
} }

@ -639,24 +639,38 @@ public abstract class BukkitData implements Data {
private double health; private double health;
@SerializedName("health_scale") @SerializedName("health_scale")
private double healthScale; private double healthScale;
@SerializedName("is_health_scaled")
private boolean isHealthScaled;
@NotNull @NotNull
public static BukkitData.Health from(double health, double healthScale) { public static BukkitData.Health from(double health, double scale, boolean isScaled) {
return new BukkitData.Health(health, healthScale); return new BukkitData.Health(health, scale, isScaled);
} }
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
*/
@NotNull
@Deprecated(since = "3.5.4")
public static BukkitData.Health from(double health, double scale) {
return from(health, scale, false);
}
/**
* @deprecated Use {@link #from(double, double, boolean)} instead
*/
@NotNull @NotNull
@Deprecated(forRemoval = true, since = "3.5") @Deprecated(forRemoval = true, since = "3.5")
@SuppressWarnings("unused") public static BukkitData.Health from(double health, @SuppressWarnings("unused") double max, double scale) {
public static BukkitData.Health from(double health, double maxHealth, double healthScale) { return from(health, scale, false);
return from(health, healthScale);
} }
@NotNull @NotNull
public static BukkitData.Health adapt(@NotNull Player player) { public static BukkitData.Health adapt(@NotNull Player player) {
return from( return from(
player.getHealth(), player.getHealth(),
player.isHealthScaled() ? player.getHealthScale() : 0d player.getHealthScale(),
player.isHealthScaled()
); );
} }
@ -674,13 +688,8 @@ public abstract class BukkitData implements Data {
// Set health scale // Set health scale
try { try {
if (healthScale != 0d) { player.setHealthScale(healthScale);
player.setHealthScaled(true); player.setHealthScaled(isHealthScaled);
player.setHealthScale(healthScale);
} else {
player.setHealthScaled(false);
player.setHealthScale(player.getMaxHealth());
}
} catch (Throwable e) { } catch (Throwable e) {
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e); plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e);
} }

@ -330,7 +330,7 @@ public class LegacyMigrator extends Migrator {
)) ))
// Health, hunger, experience & game mode // Health, hunger, experience & game mode
.health(BukkitData.Health.from(health, healthScale)) .health(BukkitData.Health.from(health, healthScale, false))
.hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion)) .hunger(BukkitData.Hunger.from(hunger, saturation, saturationExhaustion))
.experience(BukkitData.Experience.from(totalExp, expLevel, expProgress)) .experience(BukkitData.Experience.from(totalExp, expLevel, expProgress))
.gameMode(BukkitData.GameMode.from(gameMode)) .gameMode(BukkitData.GameMode.from(gameMode))

@ -85,7 +85,8 @@ public class BukkitLegacyConverter extends LegacyConverter {
if (shouldImport(Identifier.HEALTH)) { if (shouldImport(Identifier.HEALTH)) {
containers.put(Identifier.HEALTH, BukkitData.Health.from( containers.put(Identifier.HEALTH, BukkitData.Health.from(
status.getDouble("health"), status.getDouble("health"),
status.getDouble("health_scale") status.getDouble("health_scale"),
false
)); ));
} }
if (shouldImport(Identifier.HUNGER)) { if (shouldImport(Identifier.HUNGER)) {

@ -31,7 +31,7 @@ import net.william278.husksync.adapter.DataAdapter;
import net.william278.husksync.config.ConfigProvider; import net.william278.husksync.config.ConfigProvider;
import net.william278.husksync.data.Data; import net.william278.husksync.data.Data;
import net.william278.husksync.data.Identifier; import net.william278.husksync.data.Identifier;
import net.william278.husksync.data.Serializer; import net.william278.husksync.data.SerializerRegistry;
import net.william278.husksync.database.Database; import net.william278.husksync.database.Database;
import net.william278.husksync.event.EventDispatcher; import net.william278.husksync.event.EventDispatcher;
import net.william278.husksync.migrator.Migrator; import net.william278.husksync.migrator.Migrator;
@ -52,7 +52,7 @@ import java.util.logging.Level;
/** /**
* Abstract implementation of the HuskSync plugin. * Abstract implementation of the HuskSync plugin.
*/ */
public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider { public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider, SerializerRegistry {
int SPIGOT_RESOURCE_ID = 97144; int SPIGOT_RESOURCE_ID = 97144;
@ -98,43 +98,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
@NotNull @NotNull
DataAdapter getDataAdapter(); DataAdapter getDataAdapter();
/**
* Returns the data serializer for the given {@link Identifier}
*/
@NotNull
<T extends Data> Map<Identifier, Serializer<T>> getSerializers();
/**
* Register a data serializer for the given {@link Identifier}
*
* @param identifier the {@link Identifier}
* @param serializer the {@link Serializer}
*/
default void registerSerializer(@NotNull Identifier identifier,
@NotNull Serializer<? extends Data> serializer) {
if (identifier.isCustom()) {
log(Level.INFO, String.format("Registered custom data type: %s", identifier));
}
getSerializers().put(identifier, (Serializer<Data>) serializer);
}
/**
* Get the {@link Identifier} for the given key
*/
default Optional<Identifier> getIdentifier(@NotNull String key) {
return getSerializers().keySet().stream().filter(identifier -> identifier.toString().equals(key)).findFirst();
}
/**
* Get the set of registered data types
*
* @return the set of registered data types
*/
@NotNull
default Set<Identifier> getRegisteredDataTypes() {
return getSerializers().keySet();
}
/** /**
* Returns the data syncer implementation * Returns the data syncer implementation
* *

@ -49,7 +49,7 @@ public class GsonAdapter implements DataAdapter {
@Override @Override
@NotNull @NotNull
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException { public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
return this.fromJson(new String(data, StandardCharsets.UTF_8), type); return this.fromJson(new String(data, StandardCharsets.UTF_8), type);
} }

@ -31,7 +31,6 @@ public class SnappyGsonAdapter extends GsonAdapter {
super(plugin); super(plugin);
} }
@NotNull
@Override @Override
public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException { public <A extends Adaptable> byte[] toBytes(@NotNull A data) throws AdaptionException {
try { try {
@ -43,7 +42,7 @@ public class SnappyGsonAdapter extends GsonAdapter {
@NotNull @NotNull
@Override @Override
public <A extends Adaptable> A fromBytes(@NotNull byte[] data, @NotNull Class<A> type) throws AdaptionException { public <A extends Adaptable> A fromBytes(byte[] data, @NotNull Class<A> type) throws AdaptionException {
try { try {
return super.fromBytes(decompressBytes(data), type); return super.fromBytes(decompressBytes(data), type);
} catch (IOException e) { } catch (IOException e) {

@ -378,6 +378,17 @@ public class HuskSyncAPI {
plugin.registerSerializer(identifier, serializer); plugin.registerSerializer(identifier, serializer);
} }
/**
* Get a registered data serializer by its identifier
*
* @param identifier The identifier of the data type to get the serializer for
* @return The serializer for the given identifier, or an empty optional if the serializer isn't registered
* @since 3.5.4
*/
public Optional<Serializer<Data>> getDataSerializer(@NotNull Identifier identifier) {
return plugin.getSerializer(identifier);
}
/** /**
* Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed} * Get a {@link DataSnapshot.Unpacked} from a {@link DataSnapshot.Packed}
* *

@ -241,10 +241,19 @@ public class HuskSyncCommand extends Command implements TabProvider {
JoinConfiguration.commas(true), JoinConfiguration.commas(true),
plugin.getRegisteredDataTypes().stream().map(i -> { plugin.getRegisteredDataTypes().stream().map(i -> {
boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i); boolean enabled = plugin.getSettings().getSynchronization().isFeatureEnabled(i);
return Component.textOfChildren(Component return Component.textOfChildren(Component.text(i.toString())
.text(i.toString()).appendSpace().append(Component.text(enabled ? '✔' : '❌'))) .appendSpace().append(Component.text(enabled ? '✔' : '❌')))
.color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED) .color(enabled ? NamedTextColor.GREEN : NamedTextColor.RED)
.hoverEvent(HoverEvent.showText(Component.text(enabled ? "Enabled" : "Disabled"))); .hoverEvent(HoverEvent.showText(
Component.text(enabled ? "Enabled" : "Disabled")
.append(Component.newline())
.append(Component.text("Dependencies: %s".formatted(i.getDependencies()
.isEmpty() ? "(None)" : i.getDependencies().stream()
.map(d -> "%s (%s)".formatted(
d.getKey().value(), d.isRequired() ? "Required" : "Optional"
)).collect(Collectors.joining(", ")))
).color(NamedTextColor.GRAY))
));
}).toList() }).toList()
)); ));

@ -370,7 +370,7 @@ public class DataSnapshot {
public static class Unpacked extends DataSnapshot implements DataHolder { public static class Unpacked extends DataSnapshot implements DataHolder {
@Expose(serialize = false, deserialize = false) @Expose(serialize = false, deserialize = false)
private final Map<Identifier, Data> deserialized; private final TreeMap<Identifier, Data> deserialized;
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp, private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data, @NotNull String saveCause, @NotNull String serverName, @NotNull Map<String, String> data,
@ -381,7 +381,7 @@ public class DataSnapshot {
} }
private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp, private Unpacked(@NotNull UUID id, boolean pinned, @NotNull OffsetDateTime timestamp,
@NotNull String saveCause, @NotNull String serverName, @NotNull Map<Identifier, Data> data, @NotNull String saveCause, @NotNull String serverName, @NotNull TreeMap<Identifier, Data> data,
@NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) { @NotNull Version minecraftVersion, @NotNull String platformType, int formatVersion) {
super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion); super(id, pinned, timestamp, saveCause, serverName, Map.of(), minecraftVersion, platformType, formatVersion);
this.deserialized = data; this.deserialized = data;
@ -389,25 +389,25 @@ public class DataSnapshot {
@NotNull @NotNull
@ApiStatus.Internal @ApiStatus.Internal
private Map<Identifier, Data> deserializeData(@NotNull HuskSync plugin) { private TreeMap<Identifier, Data> deserializeData(@NotNull HuskSync plugin) {
return data.entrySet().stream() return data.entrySet().stream()
.map((entry) -> plugin.getIdentifier(entry.getKey()).map(id -> Map.entry( .filter(e -> plugin.getIdentifier(e.getKey()).isPresent())
id, plugin.getSerializers().get(id).deserialize(entry.getValue(), getMinecraftVersion()) .map(entry -> Map.entry(plugin.getIdentifier(entry.getKey()).orElseThrow(), entry.getValue()))
)).orElse(null)) .collect(Collectors.toMap(
.filter(Objects::nonNull) Map.Entry::getKey,
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); entry -> plugin.deserializeData(entry.getKey(), entry.getValue()),
(a, b) -> b, () -> Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR)
));
} }
@NotNull @NotNull
@ApiStatus.Internal @ApiStatus.Internal
private Map<String, String> serializeData(@NotNull HuskSync plugin) { private Map<String, String> serializeData(@NotNull HuskSync plugin) {
return deserialized.entrySet().stream() return deserialized.entrySet().stream()
.map((entry) -> Map.entry(entry.getKey().toString(), .collect(Collectors.toMap(
Objects.requireNonNull( entry -> entry.getKey().toString(),
plugin.getSerializers().get(entry.getKey()), entry -> plugin.serializeData(entry.getKey(), entry.getValue())
String.format("No serializer found for %s", entry.getKey()) ));
).serialize(entry.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
} }
/** /**
@ -453,12 +453,12 @@ public class DataSnapshot {
private String serverName; private String serverName;
private boolean pinned; private boolean pinned;
private OffsetDateTime timestamp; private OffsetDateTime timestamp;
private final Map<Identifier, Data> data; private final TreeMap<Identifier, Data> data;
private Builder(@NotNull HuskSync plugin) { private Builder(@NotNull HuskSync plugin) {
this.plugin = plugin; this.plugin = plugin;
this.pinned = false; this.pinned = false;
this.data = Maps.newHashMap(); this.data = Maps.newTreeMap(SerializerRegistry.DEPENDENCY_ORDER_COMPARATOR);
this.timestamp = OffsetDateTime.now(); this.timestamp = OffsetDateTime.now();
this.id = UUID.randomUUID(); this.id = UUID.randomUUID();
this.serverName = plugin.getServerName(); this.serverName = plugin.getServerName();

@ -19,55 +19,93 @@
package net.william278.husksync.data; package net.william278.husksync.data;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import net.kyori.adventure.key.InvalidKeyException; import net.kyori.adventure.key.InvalidKeyException;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import org.intellij.lang.annotations.Subst; import org.intellij.lang.annotations.Subst;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Collections;
import java.util.Comparator;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
/** /**
* Identifiers of different types of {@link Data}s * Identifiers of different types of {@link Data}s
*/ */
@Getter
public class Identifier { public class Identifier {
public static Identifier INVENTORY = huskSync("inventory", true); // Built-in identifiers
public static Identifier ENDER_CHEST = huskSync("ender_chest", true); public static final Identifier PERSISTENT_DATA = huskSync("persistent_data", true);
public static Identifier POTION_EFFECTS = huskSync("potion_effects", true); public static final Identifier INVENTORY = huskSync("inventory", true);
public static Identifier ADVANCEMENTS = huskSync("advancements", true); public static final Identifier ENDER_CHEST = huskSync("ender_chest", true);
public static Identifier LOCATION = huskSync("location", false); public static final Identifier ADVANCEMENTS = huskSync("advancements", true);
public static Identifier STATISTICS = huskSync("statistics", true); public static final Identifier STATISTICS = huskSync("statistics", true);
public static Identifier HEALTH = huskSync("health", true); public static final Identifier POTION_EFFECTS = huskSync("potion_effects", true);
public static Identifier HUNGER = huskSync("hunger", true); public static final Identifier GAME_MODE = huskSync("game_mode", false);
public static Identifier ATTRIBUTES = huskSync("attributes", true); public static final Identifier FLIGHT_STATUS = huskSync("flight_status", true,
public static Identifier EXPERIENCE = huskSync("experience", true); Dependency.optional("game_mode")
public static Identifier GAME_MODE = huskSync("game_mode", true); );
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true); public static final Identifier ATTRIBUTES = huskSync("attributes", true,
public static Identifier PERSISTENT_DATA = huskSync("persistent_data", true); Dependency.required("potion_effects")
);
public static final Identifier HEALTH = huskSync("health", true,
Dependency.optional("attributes")
);
public static final Identifier HUNGER = huskSync("hunger", true,
Dependency.optional("attributes")
);
public static final Identifier EXPERIENCE = huskSync("experience", true,
Dependency.optional("advancements")
);
public static final Identifier LOCATION = huskSync("location", false,
Dependency.optional("flight_status"),
Dependency.optional("potion_effects")
);
private final Key key; private final Key key;
private final boolean configDefault; private final boolean enabledByDefault;
@Getter
private final Set<Dependency> dependencies;
private Identifier(@NotNull Key key, boolean configDefault) { private Identifier(@NotNull Key key, boolean enabledByDefault, @NotNull Set<Dependency> dependencies) {
this.key = key; this.key = key;
this.configDefault = configDefault; this.enabledByDefault = enabledByDefault;
this.dependencies = dependencies;
} }
/** /**
* Create an identifier from a {@link Key} * Create an identifier from a {@link Key}
* *
* @param key the key * @param key the key
* @param dependencies the dependencies
* @return the identifier * @return the identifier
* @since 3.0 * @since 3.5.4
*/ */
@NotNull @NotNull
public static Identifier from(@NotNull Key key) { public static Identifier from(@NotNull Key key, @NotNull Set<Dependency> dependencies) {
if (key.namespace().equals("husksync")) { if (key.namespace().equals("husksync")) {
throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!"); throw new IllegalArgumentException("You cannot register a key with \"husksync\" as the namespace!");
} }
return new Identifier(key, true); return new Identifier(key, true, dependencies);
}
/**
* Create an identifier from a {@link Key}
*
* @param key the key
* @return the identifier
* @since 3.0
*/
@NotNull
public static Identifier from(@NotNull Key key) {
return from(key, Collections.emptySet());
} }
/** /**
@ -83,25 +121,34 @@ public class Identifier {
return from(Key.key(plugin, name)); return from(Key.key(plugin, name));
} }
/**
* Create an identifier from a namespace, value, and dependencies
*
* @param plugin the namespace
* @param name the value
* @param dependencies the dependencies
* @return the identifier
* @since 3.5.4
*/
@NotNull @NotNull
private static Identifier huskSync(@Subst("null") @NotNull String name, public static Identifier from(@Subst("plugin") @NotNull String plugin, @Subst("null") @NotNull String name,
boolean configDefault) throws InvalidKeyException { @NotNull Set<Dependency> dependencies) {
return new Identifier(Key.key("husksync", name), configDefault); return from(Key.key(plugin, name), dependencies);
} }
// Return an identifier with a HuskSync namespace
@NotNull @NotNull
@SuppressWarnings("unused") private static Identifier huskSync(@Subst("null") @NotNull String name,
private static Identifier parse(@NotNull String key) throws InvalidKeyException { boolean configDefault) throws InvalidKeyException {
return huskSync(key, true); return new Identifier(Key.key("husksync", name), configDefault, Collections.emptySet());
}
public boolean isEnabledByDefault() {
return configDefault;
} }
// Return an identifier with a HuskSync namespace
@NotNull @NotNull
private Map.Entry<String, Boolean> getConfigEntry() { private static Identifier huskSync(@Subst("null") @NotNull String name,
return Map.entry(getKeyValue(), configDefault); @SuppressWarnings("SameParameterValue") boolean configDefault,
@NotNull Dependency... dependents) throws InvalidKeyException {
return new Identifier(Key.key("husksync", name), configDefault, Set.of(dependents));
} }
/** /**
@ -122,6 +169,17 @@ public class Identifier {
.toArray(Map.Entry[]::new)); .toArray(Map.Entry[]::new));
} }
/**
* Returns {@code true} if the identifier depends on the given identifier
*
* @param identifier the identifier to check
* @return {@code true} if the identifier depends on the given identifier
* @since 3.5.4
*/
public boolean dependsOn(@NotNull Identifier identifier) {
return dependencies.contains(Dependency.required(identifier.key));
}
/** /**
* Get the namespace of the identifier * Get the namespace of the identifier
* *
@ -176,4 +234,85 @@ public class Identifier {
return false; return false;
} }
// Get the config entry for the identifier
@NotNull
private Map.Entry<String, Boolean> getConfigEntry() {
return Map.entry(getKeyValue(), enabledByDefault);
}
/**
* Compares two identifiers based on their dependencies.
* <p>
* If this identifier contains a dependency on the other, it should come after & vice versa
*
* @since 3.5.4
*/
@NoArgsConstructor(access = AccessLevel.PACKAGE)
static class DependencyOrderComparator implements Comparator<Identifier> {
@Override
public int compare(@NotNull Identifier i1, @NotNull Identifier i2) {
if (i1.equals(i2)) {
return 0;
}
if (i1.dependsOn(i2)) {
if (i2.dependsOn(i1)) {
throw new IllegalArgumentException(
"Found circular dependency between %s and %s".formatted(i1.getKey(), i2.getKey())
);
}
return 1;
}
return -1;
}
}
/**
* Represents a data dependency of an identifier
*
* @since 3.5.4
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public static class Dependency {
/**
* Key of the data dependency see {@code Identifier#key()}
*/
private Key key;
/**
* Whether the data dependency is required to be present & enabled for the dependant data to enabled
*/
private boolean required;
@NotNull
protected static Dependency required(@NotNull Key identifier) {
return new Dependency(identifier, true);
}
@NotNull
public static Dependency optional(@NotNull Key identifier) {
return new Dependency(identifier, false);
}
@NotNull
@SuppressWarnings("SameParameterValue")
private static Dependency required(@Subst("null") @NotNull String identifier) {
return required(Key.key("husksync", identifier));
}
@NotNull
private static Dependency optional(@Subst("null") @NotNull String identifier) {
return optional(Key.key("husksync", identifier));
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Dependency other) {
return key.equals(other.key);
}
return false;
}
}
} }

@ -0,0 +1,162 @@
/*
* This file is part of HuskSync, licensed under the Apache License 2.0.
*
* Copyright (c) William278 <will27528@gmail.com>
* Copyright (c) contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.william278.husksync.data;
import net.william278.husksync.HuskSync;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.*;
import java.util.logging.Level;
public interface SerializerRegistry {
// Comparator for ordering identifiers based on dependency
@NotNull
@ApiStatus.Internal
Comparator<Identifier> DEPENDENCY_ORDER_COMPARATOR = new Identifier.DependencyOrderComparator();
/**
* Returns the data serializer for the given {@link Identifier}
*
* @since 3.0
*/
@NotNull
<T extends Data> TreeMap<Identifier, Serializer<T>> getSerializers();
/**
* Register a data serializer for the given {@link Identifier}
*
* @param identifier the {@link Identifier}
* @param serializer the {@link Serializer}
* @since 3.0
*/
@SuppressWarnings("unchecked")
default void registerSerializer(@NotNull Identifier identifier,
@NotNull Serializer<? extends Data> serializer) {
if (identifier.isCustom()) {
getPlugin().log(Level.INFO, "Registered custom data type: %s".formatted(identifier));
}
getSerializers().put(identifier, (Serializer<Data>) serializer);
}
/**
* Ensure dependencies for identifiers that have required dependencies are met
* <p>
* This checks the dependencies of all registered identifiers and throws an {@link IllegalStateException}
* if a dependency has not been registered or enabled via the config
*
* @since 3.5.4
*/
default void validateDependencies() throws IllegalStateException {
getSerializers().keySet().stream().filter(this::isDataTypeEnabled)
.forEach(identifier -> {
final List<String> unmet = identifier.getDependencies().stream()
.filter(Identifier.Dependency::isRequired)
.filter(dep -> !isDataTypeAvailable(dep.getKey().asString()))
.map(dep -> dep.getKey().asString()).toList();
if (!unmet.isEmpty()) {
throw new IllegalStateException(
"\"%s\" data requires the following disabled data types to facilitate syncing: %s"
.formatted(identifier, String.join(", ", unmet))
);
}
});
}
/**
* Get the {@link Identifier} for the given key
*
* @since 3.0
*/
default Optional<Identifier> getIdentifier(@NotNull String key) {
return getSerializers().keySet().stream()
.filter(id -> id.getKey().asString().equals(key)).findFirst();
}
/**
* Get a data serializer for the given {@link Identifier}
*
* @param identifier the {@link Identifier} to get the serializer for
* @return the {@link Serializer} for the given {@link Identifier}
* @since 3.5.4
*/
default Optional<Serializer<Data>> getSerializer(@NotNull Identifier identifier) {
return getSerializers().entrySet().stream()
.filter(entry -> entry.getKey().getKey().equals(identifier.getKey()))
.map(Map.Entry::getValue).findFirst();
}
/**
* Serialize data for the given {@link Identifier}
*
* @param identifier the {@link Identifier} to serialize data for
* @param data the data to serialize
* @return the serialized data
* @throws IllegalArgumentException if no serializer is found for the given {@link Identifier}
* @since 3.5.4
*/
@NotNull
default String serializeData(@NotNull Identifier identifier, @NotNull Data data) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.serialize(data))
.orElseThrow(() -> new IllegalStateException("No serializer found for %s".formatted(identifier)));
}
/**
* Deserialize data for the given {@link Identifier}
*
* @param identifier the {@link Identifier} to deserialize data for
* @param data the data to deserialize
* @return the deserialized data
* @throws IllegalStateException if no serializer is found for the given {@link Identifier}
* @since 3.5.4
*/
@NotNull
default Data deserializeData(@NotNull Identifier identifier, @NotNull String data) throws IllegalStateException {
return getSerializer(identifier).map(serializer -> serializer.deserialize(data)).orElseThrow(
() -> new IllegalStateException("No serializer found for %s".formatted(identifier))
);
}
/**
* Get the set of registered data types
*
* @return the set of registered data types
* @since 3.0
*/
@NotNull
default Set<Identifier> getRegisteredDataTypes() {
return getSerializers().keySet();
}
// Returns if a data type is available and enabled in the config
private boolean isDataTypeAvailable(@NotNull String key) {
return getIdentifier(key).map(this::isDataTypeEnabled).orElse(false);
}
// Returns if a data type is enabled in the config
private boolean isDataTypeEnabled(@NotNull Identifier identifier) {
return getPlugin().getSettings().getSynchronization().isFeatureEnabled(identifier);
}
@NotNull
HuskSync getPlugin();
}

@ -34,7 +34,7 @@ import java.util.logging.Level;
public interface UserDataHolder extends DataHolder { public interface UserDataHolder extends DataHolder {
/** /**
* Get the data that is enabled for syncing in the config * Get the data enabled for syncing in the config
* *
* @return the data that is enabled for syncing * @return the data that is enabled for syncing
* @since 3.0 * @since 3.0

@ -116,12 +116,26 @@ public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login
huskSyncAPI.registerSerializer(LOGIN_PARTICLES_ID, new LoginParticleSerializer(HuskSyncAPI.getInstance())); huskSyncAPI.registerSerializer(LOGIN_PARTICLES_ID, new LoginParticleSerializer(HuskSyncAPI.getInstance()));
``` ```
### 3.1 Identifier dependencies
* HuskSync lets you specify a set of `Dependency` objects when creating an `Identifier`. These are used to deterministically apply data in a specific order.
* Dependencies are references to other data type identifiers. HuskSync will apply data in dependency-order; that is, it will apply the data of the dependencies before applying the data of the dependent.
* This is useful when you have data that relies on other data to be applied first; for example, if you're writing an add-on for additional modded inventory data and you need to apply the base inventory data first.
* You can specify whether a dependency is required or optional. HuskSync will not sync data of a type that has a required dependency that is missing (for instance, if it is disabled in the config, or - if provided by another plugin - has failed to register).
* Use `Identifer#from(String, String, Set<Dependency>)` or `Identifier#from(Key, Set<Dependency>)` to create an identifier with dependencies
* Dependencies can be created with `Dependency.optional(Identifier)` or `Dependency.required(Identifier)` for optional or required dependencies respectively.
## 4. Setting and getting our Data to/from a User ## 4. Setting and getting our Data to/from a User
* Now that we've registered our `Data` and `Serializer` classes, we can set our data to a user, applying it to them. * Now that we've registered our `Data` and `Serializer` classes, we can set our data to a user, applying it to them.
* To do this, we use the `OnlineUser#setData(Identifier, Data)` method. * To do this, we use the `OnlineUser#setData(Identifier, Data)` method.
* This method will apply the data to the user, and store the data to the plugin player custom data map, to allow the data to be retrieved later and be saved to snapshots. * This method will apply the data to the user, and store the data to the plugin player custom data map, to allow the data to be retrieved later and be saved to snapshots.
* Snapshots created on servers where the data type is registered will now contain our data and synchronise between instances! * Snapshots created on servers where the data type is registered will now contain our data and synchronise between instances!
```java
// Create an identifier for our data requiring the user's location to have been set first
public static Identifier LOGIN_PARTICLES_ID = Identifier.from("myplugin", "login_particles", Set.of(Dependency.optional(Key.key("husksync", "location"))));
// We can then register this as we did previously (...)
```
```java ```java
// Create an instance of our data // Create an instance of our data
LoginParticleData loginParticleData = new LoginParticleData("FIREWORKS_SPARK", 10); LoginParticleData loginParticleData = new LoginParticleData("FIREWORKS_SPARK", 10);

@ -213,8 +213,8 @@ huskSyncAPI.getCurrentData(user).thenAccept(optionalSnapshot -> {
// Get the health data // Get the health data
Data.Health health = healthOptional.get(); Data.Health health = healthOptional.get();
double currentHealth = health.getCurrentHealth(); // Current health double currentHealth = health.getCurrentHealth(); // Current health
double healthScale = health.getHealthScale(); // Health scale (e.g., 20 for 20 hearts) double healthScale = health.getHealthScale(); // Health scale (used to determine health/damage display hearts)
snapshot.setHealth(BukkitData.Health.from(20, 20)); snapshot.setHealth(BukkitData.Health.from(20, 20, true));
// Need max health? Look at the Attributes data type. // Need max health? Look at the Attributes data type.
// Get the game mode data // Get the game mode data

Loading…
Cancel
Save