9
0
mirror of https://github.com/WiIIiam278/HuskSync.git synced 2026-01-04 15:31:37 +00:00

feat: Add attribute syncing (#276)

* refactor: add attribute syncing

* fix: don't sync unmodified attributes

* fix: register json serializer for Attributes

* fix: improve Attribute API methods

* docs: update Sync Features

* refactor: make attributes a set

Because they're unique (by UUID)
This commit is contained in:
William
2024-04-10 19:38:37 +01:00
committed by GitHub
parent 82dc765f66
commit 676ba7a10a
13 changed files with 233 additions and 108 deletions

View File

@@ -143,6 +143,7 @@ public class BukkitHuskSync extends JavaPlugin implements HuskSync, BukkitTask.S
registerSerializer(Identifier.LOCATION, new BukkitSerializer.Location(this));
registerSerializer(Identifier.HEALTH, new BukkitSerializer.Json<>(this, BukkitData.Health.class));
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.FLIGHT_STATUS, new BukkitSerializer.Json<>(this, BukkitData.FlightStatus.class));
registerSerializer(Identifier.POTION_EFFECTS, new BukkitSerializer.PotionEffects(this));

View File

@@ -32,11 +32,12 @@ import net.william278.husksync.adapter.Adaptable;
import net.william278.husksync.user.BukkitUser;
import org.bukkit.*;
import org.bukkit.advancement.AdvancementProgress;
import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.entity.EntityType;
import org.bukkit.entity.Player;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.potion.PotionEffect;
@@ -431,7 +432,7 @@ public abstract class BukkitData implements Data {
}
// TODO: Consider using Paper's new-ish API for this instead (when it's merged)
// TODO: Move to using Registry.STATISTIC as soon as possible!
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Statistics extends BukkitData implements Data.Statistics {
@@ -667,6 +668,73 @@ public abstract class BukkitData implements Data {
container.mergeCompound(persistentData);
}
}
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public static class Attributes extends BukkitData implements Data.Attributes, Adaptable {
private List<Attribute> attributes;
@NotNull
public static BukkitData.Attributes adapt(@NotNull Player player) {
final List<Attribute> attributes = Lists.newArrayList();
Registry.ATTRIBUTE.forEach(id -> {
final AttributeInstance instance = player.getAttribute(id);
if (instance == null || instance.getValue() == instance.getDefaultValue()) {
return; // We don't sync unmodified attributes
}
attributes.add(adapt(instance));
});
return new BukkitData.Attributes(attributes);
}
public Optional<Attribute> getAttribute(@NotNull org.bukkit.attribute.Attribute id) {
return attributes.stream().filter(attribute -> attribute.name().equals(id.getKey().toString())).findFirst();
}
@NotNull
private static Attribute adapt(@NotNull AttributeInstance instance) {
return new Attribute(
instance.getAttribute().getKey().toString(),
instance.getBaseValue(),
instance.getModifiers().stream().map(BukkitData.Attributes::adapt).collect(Collectors.toSet())
);
}
@NotNull
private static Modifier adapt(@NotNull AttributeModifier modifier) {
return new Modifier(
modifier.getUniqueId(),
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)));
}
private static void applyAttribute(@Nullable AttributeInstance instance, @Nullable Attribute attribute) {
if (instance == null) {
return;
}
instance.setBaseValue(attribute == null ? instance.getDefaultValue() : instance.getBaseValue());
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
)));
}
}
}
@@ -677,88 +745,55 @@ public abstract class BukkitData implements Data {
public static class Health extends BukkitData implements Data.Health, Adaptable {
@SerializedName("health")
private double health;
@SerializedName("max_health")
private double maxHealth;
@SerializedName("health_scale")
private double healthScale;
@NotNull
public static BukkitData.Health from(double health, double healthScale) {
return new BukkitData.Health(health, healthScale);
}
@NotNull
@Deprecated(forRemoval = true, since = "3.5")
@SuppressWarnings("unused")
public static BukkitData.Health from(double health, double maxHealth, double healthScale) {
return new BukkitData.Health(health, maxHealth, healthScale);
return from(health, healthScale);
}
@NotNull
public static BukkitData.Health adapt(@NotNull Player player) {
return from(
player.getHealth(),
getMaxHealth(player),
player.isHealthScaled() ? player.getHealthScale() : 0d
);
}
@Override
@SuppressWarnings("deprecation")
public void apply(@NotNull BukkitUser user, @NotNull BukkitHuskSync plugin) throws IllegalStateException {
final Player player = user.getPlayer();
// Set max health
final AttributeInstance maxHealth = getMaxHealthAttribute(player);
try {
if (plugin.getSettings().getSynchronization().isSynchronizeMaxHealth() && this.maxHealth != 0) {
maxHealth.setBaseValue(this.maxHealth);
}
} catch (Throwable e) {
plugin.log(Level.WARNING, String.format("Failed setting the max health of %s to %s",
player.getName(), this.maxHealth), e);
}
// Set health
try {
final double health = player.getHealth();
player.setHealth(Math.min(health, maxHealth.getBaseValue()));
player.setHealth(Math.min(health, player.getMaxHealth()));
} catch (Throwable e) {
plugin.log(Level.WARNING, String.format("Failed setting the health of %s to %s",
player.getName(), this.maxHealth), e);
plugin.log(Level.WARNING, "Error setting %s's health to %s".formatted(player.getName(), health), e);
}
// Set health scale
try {
if (this.healthScale != 0d) {
if (healthScale != 0d) {
player.setHealthScaled(true);
player.setHealthScale(this.healthScale);
player.setHealthScale(healthScale);
} else {
player.setHealthScaled(false);
player.setHealthScale(this.maxHealth);
player.setHealthScale(player.getMaxHealth());
}
} catch (Throwable e) {
plugin.log(Level.WARNING, String.format("Failed setting the health scale of %s to %s",
player.getName(), this.healthScale), e);
plugin.log(Level.WARNING, "Error setting %s's health scale to %s".formatted(player.getName(), healthScale), e);
}
}
// Returns the max health of a player, accounting for health boost potion effects
private static double getMaxHealth(@NotNull Player player) {
// Get the base value of the attribute (ignore armor, items that give health boosts, etc.)
double maxHealth = getMaxHealthAttribute(player).getBaseValue();
// Subtract health boost potion effects from stored max health
if (player.hasPotionEffect(PotionEffectType.HEALTH_BOOST) && maxHealth > 20d) {
final PotionEffect healthBoost = Objects.requireNonNull(
player.getPotionEffect(PotionEffectType.HEALTH_BOOST), "Health boost effect was null"
);
maxHealth -= (4 * (healthBoost.getAmplifier() + 1));
}
return maxHealth;
}
// Returns the max health attribute of a player
@NotNull
private static AttributeInstance getMaxHealthAttribute(@NotNull Player player) {
return Objects.requireNonNull(
player.getAttribute(Attribute.GENERIC_MAX_HEALTH), "Max health attribute was null"
);
}
}
@Getter

View File

@@ -41,6 +41,7 @@ public interface BukkitUserDataHolder extends UserDataHolder {
case "statistics" -> getStatistics();
case "health" -> getHealth();
case "hunger" -> getHunger();
case "attributes" -> getAttributes();
case "experience" -> getExperience();
case "game_mode" -> getGameMode();
case "flight_status" -> getFlightStatus();
@@ -117,6 +118,12 @@ public interface BukkitUserDataHolder extends UserDataHolder {
return Optional.of(BukkitData.Hunger.adapt(getBukkitPlayer()));
}
@NotNull
@Override
default Optional<Data.Attributes> getAttributes() {
return Optional.of(BukkitData.Attributes.adapt(getBukkitPlayer()));
}
@NotNull
@Override
default Optional<Data.Experience> getExperience() {

View File

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

View File

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