From 2fcd58fc184c8d033bf4f44429bf34bc361f95f9 Mon Sep 17 00:00:00 2001 From: William Date: Fri, 21 Jun 2024 13:17:53 +0100 Subject: [PATCH] feat: correctly apply keyed attribute modifiers, close #326 We need to construct attributes with their key if possible to avoid stacking. Uses reflection :( to do this. Also adds a bit of error checking to health scale syncing --- .../william278/husksync/data/BukkitData.java | 89 ++++++++++++++----- .../net/william278/husksync/data/Data.java | 30 +++---- gradle.properties | 2 +- paper/build.gradle | 2 +- 4 files changed, 86 insertions(+), 37 deletions(-) diff --git a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java index 62bf87f1..26b04142 100644 --- a/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java +++ b/bukkit/src/main/java/net/william278/husksync/data/BukkitData.java @@ -25,6 +25,7 @@ import com.google.gson.annotations.SerializedName; import de.tr7zw.changeme.nbtapi.NBTCompound; import de.tr7zw.changeme.nbtapi.NBTPersistentDataContainer; import lombok.*; +import net.kyori.adventure.util.TriState; import net.william278.desertwell.util.ThrowingConsumer; import net.william278.desertwell.util.Version; import net.william278.husksync.BukkitHuskSync; @@ -35,6 +36,7 @@ import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Registry; import org.bukkit.Statistic; +import org.bukkit.NamespacedKey; import org.bukkit.advancement.AdvancementProgress; import org.bukkit.attribute.AttributeInstance; import org.bukkit.attribute.AttributeModifier; @@ -565,6 +567,11 @@ public abstract class BukkitData implements Data { @NoArgsConstructor(access = AccessLevel.PRIVATE) public static class Attributes extends BukkitData implements Data.Attributes, Adaptable { + private static final String EQUIPMENT_SLOT_GROUP = "org.bukkit.inventory.EquipmentSlotGroup"; + private static final String EQUIPMENT_SLOT_GROUP$ANY = "ANY"; + private static final String EQUIPMENT_SLOT$getGroup = "getGroup"; + private static TriState USE_KEYED_MODIFIERS = TriState.NOT_SET; + private List attributes; @NotNull @@ -572,12 +579,12 @@ public abstract class BukkitData implements Data { final List attributes = Lists.newArrayList(); Registry.ATTRIBUTE.forEach(id -> { final AttributeInstance instance = player.getAttribute(id); - if (instance == null || instance.getValue() == instance.getDefaultValue() || plugin - .getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) { + if (instance == null || Double.compare(instance.getValue(), instance.getDefaultValue()) == 0 + || plugin.getSettings().getSynchronization().isIgnoredAttribute(id.getKey().toString())) { // We don't sync unmodified or disabled attributes return; } - attributes.add(adapt(instance, plugin.getMinecraftVersion())); + attributes.add(adapt(instance)); }); return new BukkitData.Attributes(attributes); } @@ -596,18 +603,18 @@ public abstract class BukkitData implements Data { } @NotNull - private static Attribute adapt(@NotNull AttributeInstance instance, @NotNull Version version) { + private static Attribute adapt(@NotNull AttributeInstance instance) { return new Attribute( instance.getAttribute().getKey().toString(), instance.getBaseValue(), - instance.getModifiers().stream().map(m -> adapt(m, version)).collect(Collectors.toSet()) + instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet()) ); } @NotNull - private static Modifier adapt(@NotNull AttributeModifier modifier, @NotNull Version version) { + private static Modifier adapt(@NotNull AttributeModifier modifier) { return new Modifier( - version.compareTo(Version.fromString("1.21")) >= 0 ? null : modifier.getUniqueId(), + getModifierId(modifier), modifier.getName(), modifier.getAmount(), modifier.getOperation().ordinal(), @@ -615,26 +622,67 @@ public abstract class BukkitData implements Data { ); } - @Override - public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException { - Registry.ATTRIBUTE.forEach(id -> applyAttribute(user.getPlayer().getAttribute(id), getAttribute(id).orElse(null))); + @Nullable + private static UUID getModifierId(@NotNull AttributeModifier modifier) { + try { + return UUID.fromString(modifier.getName()); + } catch (Throwable e) { + return null; + } } - private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) { + private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute, + @NotNull HuskSync plugin) { if (instance == null) { return; } instance.setBaseValue(attribute == null ? instance.getDefaultValue() : attribute.baseValue()); instance.getModifiers().forEach(instance::removeModifier); if (attribute != null) { - attribute.modifiers().forEach(modifier -> instance.addModifier(new AttributeModifier( - modifier.uuid(), - modifier.name(), - modifier.amount(), - AttributeModifier.Operation.values()[modifier.operationType()], - modifier.equipmentSlot() != -1 ? EquipmentSlot.values()[modifier.equipmentSlot()] : null - ))); + attribute.modifiers().forEach(modifier -> instance.addModifier(adapt(modifier, plugin))); + } + } + + @SuppressWarnings("JavaReflectionMemberAccess") + @NotNull + private static AttributeModifier adapt(@NotNull Modifier modifier, @NotNull HuskSync plugin) { + final int slotId = modifier.equipmentSlot(); + if (USE_KEYED_MODIFIERS == TriState.NOT_SET) { + USE_KEYED_MODIFIERS = TriState.byBoolean(plugin.getMinecraftVersion() + .compareTo(Version.fromString("1.21")) >= 0); + } + if (USE_KEYED_MODIFIERS == TriState.TRUE) { + try { + // Reflexively create a modern keyed attribute modifier instance. Remove in favor of API long-term. + final EquipmentSlot slot = slotId != -1 ? EquipmentSlot.values()[slotId] : null; + final Class slotGroup = Class.forName(EQUIPMENT_SLOT_GROUP); + return AttributeModifier.class.getDeclaredConstructor( + NamespacedKey.class, double.class, AttributeModifier.Operation.class, slotGroup + ).newInstance( + NamespacedKey.fromString(modifier.name()), modifier.amount(), + AttributeModifier.Operation.values()[modifier.operationType()], + slot == null ? slotGroup.getField(EQUIPMENT_SLOT_GROUP$ANY).get(null) + : EquipmentSlot.class.getDeclaredMethod(EQUIPMENT_SLOT$getGroup).invoke(slot) + ); + } catch (Throwable e) { + plugin.log(Level.WARNING, "Error reflectively creating keyed attribute modifier", e); + USE_KEYED_MODIFIERS = TriState.FALSE; + } } + return new AttributeModifier( + modifier.uuid(), + modifier.name(), + modifier.amount(), + AttributeModifier.Operation.values()[modifier.operationType()], + slotId != -1 ? EquipmentSlot.values()[slotId] : null + ); + } + + @Override + public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException { + Registry.ATTRIBUTE.forEach(id -> applyAttribute( + user.getPlayer().getAttribute(id), getAttribute(id).orElse(null), plugin + )); } } @@ -696,11 +744,12 @@ public abstract class BukkitData implements Data { } // Set health scale + double scale = healthScale <= 0 ? player.getMaxHealth() : healthScale; try { - player.setHealthScale(healthScale); + player.setHealthScale(scale); player.setHealthScaled(isHealthScaled); } 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(), scale), e); } } diff --git a/common/src/main/java/net/william278/husksync/data/Data.java b/common/src/main/java/net/william278/husksync/data/Data.java index a47725d6..cefc5c10 100644 --- a/common/src/main/java/net/william278/husksync/data/Data.java +++ b/common/src/main/java/net/william278/husksync/data/Data.java @@ -155,14 +155,14 @@ public interface Data { */ interface Advancements extends Data { + String RECIPE_ADVANCEMENT = "minecraft:recipe"; + @NotNull List getCompleted(); @NotNull default List getCompletedExcludingRecipes() { - return getCompleted().stream() - .filter(advancement -> !advancement.getKey().startsWith("minecraft:recipe")) - .collect(Collectors.toList()); + return getCompleted().stream().filter(adv -> !adv.getKey().startsWith(RECIPE_ADVANCEMENT)).toList(); } void setCompleted(@NotNull List completed); @@ -191,13 +191,13 @@ public interface Data { @NotNull private static Map adaptDateMap(@NotNull Map dateMap) { return dateMap.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime())); + .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getTime())); } @NotNull private static Map adaptLongMap(@NotNull Map dateMap) { return dateMap.entrySet().stream() - .collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue()))); + .collect(Collectors.toMap(Map.Entry::getKey, e -> new Date(e.getValue()))); } @NotNull @@ -250,9 +250,9 @@ public interface Data { void setWorld(@NotNull World world); record World( - @SerializedName("name") @NotNull String name, - @SerializedName("uuid") @NotNull UUID uuid, - @SerializedName("environment") @NotNull String environment + @SerializedName("name") @NotNull String name, + @SerializedName("uuid") @NotNull UUID uuid, + @SerializedName("environment") @NotNull String environment ) { } } @@ -324,9 +324,9 @@ public interface Data { List getAttributes(); record Attribute( - @NotNull String name, - double baseValue, - @NotNull Set modifiers + @NotNull String name, + double baseValue, + @NotNull Set modifiers ) { public double getValue() { @@ -387,8 +387,8 @@ public interface Data { default Optional getAttribute(@NotNull Key key) { return getAttributes().stream() - .filter(attribute -> attribute.name().equals(key.asString())) - .findFirst(); + .filter(attribute -> attribute.name().equals(key.asString())) + .findFirst(); } default void removeAttribute(@NotNull Key key) { @@ -397,8 +397,8 @@ public interface Data { default double getMaxHealth() { return getAttribute(MAX_HEALTH_KEY) - .map(Attribute::getValue) - .orElse(20.0); + .map(Attribute::getValue) + .orElse(20.0); } default void setMaxHealth(double maxHealth) { diff --git a/gradle.properties b/gradle.properties index d8384b0a..cedb7c44 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs='-Dfile.encoding=UTF-8' org.gradle.daemon=true javaVersion=17 -plugin_version=3.6.3 +plugin_version=3.6.4 plugin_archive=husksync plugin_description=A modern, cross-server player data synchronization system diff --git a/paper/build.gradle b/paper/build.gradle index 4b4e3201..50361a70 100644 --- a/paper/build.gradle +++ b/paper/build.gradle @@ -50,7 +50,7 @@ shadowJar { tasks { runServer { - minecraftVersion('1.20.4') + minecraftVersion('1.21') downloadPlugins { url('https://download.luckperms.net/1549/bukkit/loader/LuckPerms-Bukkit-5.4.134.jar')