Skip to content

Commit

Permalink
feat: correctly apply keyed attribute modifiers, close #326
Browse files Browse the repository at this point in the history
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
  • Loading branch information
WiIIiam278 committed Jun 21, 2024
1 parent 3d10b23 commit 2fcd58f
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 37 deletions.
89 changes: 69 additions & 20 deletions bukkit/src/main/java/net/william278/husksync/data/BukkitData.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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;
Expand All @@ -35,6 +36,7 @@
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;
Expand Down Expand Up @@ -565,19 +567,24 @@ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) thro
@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<Attribute> attributes;

@NotNull
public static BukkitData.Attributes adapt(@NotNull Player player, @NotNull HuskSync plugin) {
final List<Attribute> 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);
}
Expand All @@ -596,45 +603,86 @@ public Optional<Attribute> getAttribute(@NotNull String key) {
}

@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(),
modifier.getSlot() != null ? modifier.getSlot().ordinal() : -1
);
}

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

}
Expand Down Expand Up @@ -696,11 +744,12 @@ public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) thro
}

// 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);
}
}

Expand Down
30 changes: 15 additions & 15 deletions common/src/main/java/net/william278/husksync/data/Data.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,14 @@ record Effect(@SerializedName("type") @NotNull String type,
*/
interface Advancements extends Data {

String RECIPE_ADVANCEMENT = "minecraft:recipe";

@NotNull
List<Advancement> getCompleted();

@NotNull
default List<Advancement> 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<Advancement> completed);
Expand Down Expand Up @@ -191,13 +191,13 @@ public static Advancement adapt(@NotNull String key, @NotNull Map<String, Date>
@NotNull
private static Map<String, Long> adaptDateMap(@NotNull Map<String, Date> 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<String, Date> adaptLongMap(@NotNull Map<String, Long> 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
Expand Down Expand Up @@ -250,9 +250,9 @@ interface Location extends 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
) {
}
}
Expand Down Expand Up @@ -324,9 +324,9 @@ interface Attributes extends Data {
List<Attribute> getAttributes();

record Attribute(
@NotNull String name,
double baseValue,
@NotNull Set<Modifier> modifiers
@NotNull String name,
double baseValue,
@NotNull Set<Modifier> modifiers
) {

public double getValue() {
Expand Down Expand Up @@ -387,8 +387,8 @@ public UUID uuid() {

default Optional<Attribute> 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) {
Expand All @@ -397,8 +397,8 @@ default void removeAttribute(@NotNull Key key) {

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) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion paper/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit 2fcd58f

Please sign in to comment.