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:
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
));
|
||||
}
|
||||
|
||||
@@ -29,9 +29,6 @@ import net.william278.desertwell.util.UpdateChecker;
|
||||
import net.william278.desertwell.util.Version;
|
||||
import net.william278.husksync.adapter.DataAdapter;
|
||||
import net.william278.husksync.config.ConfigProvider;
|
||||
import net.william278.husksync.config.Locales;
|
||||
import net.william278.husksync.config.Server;
|
||||
import net.william278.husksync.config.Settings;
|
||||
import net.william278.husksync.data.Data;
|
||||
import net.william278.husksync.data.Identifier;
|
||||
import net.william278.husksync.data.Serializer;
|
||||
@@ -180,31 +177,6 @@ public interface HuskSync extends Task.Supplier, EventDispatcher, ConfigProvider
|
||||
log(Level.INFO, "Successfully initialized " + name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Settings}
|
||||
*
|
||||
* @return the {@link Settings}
|
||||
*/
|
||||
@NotNull
|
||||
Settings getSettings();
|
||||
|
||||
void setSettings(@NotNull Settings settings);
|
||||
|
||||
@NotNull
|
||||
String getServerName();
|
||||
|
||||
void setServerName(@NotNull Server serverName);
|
||||
|
||||
/**
|
||||
* Returns the plugin {@link Locales}
|
||||
*
|
||||
* @return the {@link Locales}
|
||||
*/
|
||||
@NotNull
|
||||
Locales getLocales();
|
||||
|
||||
void setLocales(@NotNull Locales locales);
|
||||
|
||||
/**
|
||||
* Returns if a dependency is loaded
|
||||
*
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
|
||||
package net.william278.husksync.data;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import net.william278.husksync.HuskSync;
|
||||
import net.william278.husksync.user.OnlineUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -286,15 +288,97 @@ public interface Data {
|
||||
|
||||
void setHealth(double health);
|
||||
|
||||
double getMaxHealth();
|
||||
/**
|
||||
* @deprecated Use {@link Attributes#getMaxHealth()} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default double getMaxHealth() {
|
||||
return getHealth();
|
||||
}
|
||||
|
||||
void setMaxHealth(double maxHealth);
|
||||
/**
|
||||
* @deprecated Use {@link Attributes#setMaxHealth(double)} instead
|
||||
*/
|
||||
@Deprecated(forRemoval = true, since = "3.5")
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
}
|
||||
|
||||
double getHealthScale();
|
||||
|
||||
void setHealthScale(double healthScale);
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding player attribute data
|
||||
*/
|
||||
interface Attributes extends Data {
|
||||
|
||||
Key MAX_HEALTH_KEY = Key.key("generic.max_health");
|
||||
|
||||
List<Attribute> getAttributes();
|
||||
|
||||
record Attribute(
|
||||
@NotNull String name,
|
||||
double baseValue,
|
||||
@NotNull Set<Modifier> modifiers
|
||||
) {
|
||||
|
||||
public double getValue() {
|
||||
double value = baseValue;
|
||||
for (Modifier modifier : modifiers) {
|
||||
value = modifier.modify(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Modifier(
|
||||
@NotNull UUID uuid,
|
||||
@NotNull String name,
|
||||
double amount,
|
||||
@SerializedName("operation") int operationType,
|
||||
@SerializedName("equipment_slot") int equipmentSlot
|
||||
) {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof Modifier modifier && modifier.uuid.equals(uuid);
|
||||
}
|
||||
|
||||
public double modify(double value) {
|
||||
return switch (operationType) {
|
||||
case 0 -> value + amount;
|
||||
case 1 -> value * amount;
|
||||
case 2 -> value * (1 + amount);
|
||||
default -> value;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
default Optional<Attribute> getAttribute(@NotNull Key key) {
|
||||
return getAttributes().stream()
|
||||
.filter(attribute -> attribute.name().equals(key.asString()))
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
default void removeAttribute(@NotNull Key key) {
|
||||
getAttributes().removeIf(attribute -> attribute.name().equals(key.asString()));
|
||||
}
|
||||
|
||||
default double getMaxHealth() {
|
||||
return getAttribute(MAX_HEALTH_KEY)
|
||||
.map(Attribute::getValue)
|
||||
.orElse(20.0);
|
||||
}
|
||||
|
||||
default void setMaxHealth(double maxHealth) {
|
||||
removeAttribute(MAX_HEALTH_KEY);
|
||||
getAttributes().add(new Attribute(MAX_HEALTH_KEY.asString(), maxHealth, Sets.newHashSet()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A data container holding data for:
|
||||
* <ul>
|
||||
|
||||
@@ -110,6 +110,15 @@ public interface DataHolder {
|
||||
getData().put(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Attributes> getAttributes() {
|
||||
return Optional.ofNullable((Data.Attributes) getData().get(Identifier.ATTRIBUTES));
|
||||
}
|
||||
|
||||
default void setAttributes(@NotNull Data.Attributes attributes) {
|
||||
getData().put(Identifier.ATTRIBUTES, attributes);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
default Optional<Data.Experience> getExperience() {
|
||||
return Optional.ofNullable((Data.Experience) getData().get(Identifier.EXPERIENCE));
|
||||
|
||||
@@ -659,6 +659,21 @@ public class DataSnapshot {
|
||||
return data(Identifier.HUNGER, hunger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the attributes of the snapshot
|
||||
* <p>
|
||||
* Equivalent to {@code data(Identifier.ATTRIBUTES, attributes)}
|
||||
* </p>
|
||||
*
|
||||
* @param attributes The user's attributes
|
||||
* @return The builder
|
||||
* @since 3.5
|
||||
*/
|
||||
@NotNull
|
||||
public Builder attributes(@NotNull Data.Attributes attributes) {
|
||||
return data(Identifier.ATTRIBUTES, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the experience of the snapshot
|
||||
* <p>
|
||||
|
||||
@@ -41,6 +41,7 @@ public class Identifier {
|
||||
public static Identifier STATISTICS = huskSync("statistics", true);
|
||||
public static Identifier HEALTH = huskSync("health", true);
|
||||
public static Identifier HUNGER = huskSync("hunger", true);
|
||||
public static Identifier ATTRIBUTES = huskSync("attributes", true);
|
||||
public static Identifier EXPERIENCE = huskSync("experience", true);
|
||||
public static Identifier GAME_MODE = huskSync("game_mode", true);
|
||||
public static Identifier FLIGHT_STATUS = huskSync("flight_status", true);
|
||||
@@ -114,8 +115,8 @@ public class Identifier {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Boolean> getConfigMap() {
|
||||
return Map.ofEntries(Stream.of(
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION,
|
||||
STATISTICS, HEALTH, HUNGER, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
|
||||
INVENTORY, ENDER_CHEST, POTION_EFFECTS, ADVANCEMENTS, LOCATION, STATISTICS,
|
||||
HEALTH, HUNGER, ATTRIBUTES, EXPERIENCE, GAME_MODE, FLIGHT_STATUS, PERSISTENT_DATA
|
||||
)
|
||||
.map(Identifier::getConfigEntry)
|
||||
.toArray(Map.Entry[]::new));
|
||||
|
||||
@@ -184,7 +184,7 @@ public class PlanHook {
|
||||
public String getHealth(@NotNull UUID uuid) {
|
||||
return getLatestSnapshot(uuid)
|
||||
.flatMap(DataHolder::getHealth)
|
||||
.map(health -> String.format("%s / %s", health.getHealth(), health.getMaxHealth()))
|
||||
.map(health -> String.format("%s", health.getHealth()))
|
||||
.orElse(UNKNOWN_STRING);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,16 +77,17 @@ public class DataSnapshotOverview {
|
||||
|
||||
// User status data, if present in the snapshot
|
||||
final Optional<Data.Health> health = snapshot.getHealth();
|
||||
final Optional<Data.Attributes> attributes = snapshot.getAttributes();
|
||||
final Optional<Data.Hunger> food = snapshot.getHunger();
|
||||
final Optional<Data.Experience> experience = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> gameMode = snapshot.getGameMode();
|
||||
if (health.isPresent() && food.isPresent() && experience.isPresent() && gameMode.isPresent()) {
|
||||
final Optional<Data.Experience> exp = snapshot.getExperience();
|
||||
final Optional<Data.GameMode> mode = snapshot.getGameMode();
|
||||
if (health.isPresent() && attributes.isPresent() && food.isPresent() && exp.isPresent() && mode.isPresent()) {
|
||||
locales.getLocale("data_manager_status",
|
||||
Integer.toString((int) health.get().getHealth()),
|
||||
Integer.toString((int) health.get().getMaxHealth()),
|
||||
Integer.toString((int) attributes.get().getMaxHealth()),
|
||||
Integer.toString(food.get().getFoodLevel()),
|
||||
Integer.toString(experience.get().getExpLevel()),
|
||||
gameMode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
Integer.toString(exp.get().getExpLevel()),
|
||||
mode.get().getGameMode().toLowerCase(Locale.ENGLISH))
|
||||
.ifPresent(user::sendMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,23 +5,24 @@ You can customise how much data HuskSync saves about a player by [turning each s
|
||||
## Feature table
|
||||
✅—Supported ❌—Unsupported ⚠️—Experimental
|
||||
|
||||
| Name | Description | Availability |
|
||||
|---------------------------|-------------------------------------------------------------|:------------:|
|
||||
| Inventories | Items in player inventories & selected hotbar slot | ✅ |
|
||||
| Ender chests | Items in ender chests* | ✅ |
|
||||
| Health | Player health points | ✅ |
|
||||
| Max health | Player max health points and health scale | ✅ |
|
||||
| Hunger | Player hunger, saturation & exhaustion | ✅ |
|
||||
| Experience | Player level, experience points & score | ✅ |
|
||||
| Potion effects | Active status effects on players | ✅ |
|
||||
| Advancements | Player advancements, recipes & progress | ✅ |
|
||||
| Game modes | Player's current game mode | ✅ |
|
||||
| Statistics | Player's in-game stats (ESC -> Statistics) | ✅ |
|
||||
| Location | Player's current coordinate positon and world† | ✅ |
|
||||
| Persistent Data Container | Custom plugin persistent data key map | ✅️ |
|
||||
| Locked maps | Maps/treasure maps locked in a cartography table | ⚠️ |
|
||||
| Unlocked maps | Regular, unlocked maps/treasure maps ([why?](#map-syncing)) | ❌ |
|
||||
| Economy balances | Vault economy balance. ([why?](#economy-syncing)) | ❌ |
|
||||
| Name | Description | Availability |
|
||||
|---------------------------|---------------------------------------------------------------------------------------------|:------------:|
|
||||
| Inventories | Items in player inventories & selected hotbar slot | ✅ |
|
||||
| Ender chests | Items in ender chests* | ✅ |
|
||||
| Health | Player health points and scale | ✅ |
|
||||
| Hunger | Player hunger, saturation & exhaustion | ✅ |
|
||||
| Attributes | Player max health, movement speed, reach, etc. ([wiki](https://minecraft.wiki/w/Attribute)) | ✅ |
|
||||
| Experience | Player level, experience points & score | ✅ |
|
||||
| Potion effects | Active status effects on players | ✅ |
|
||||
| Advancements | Player advancements, recipes & progress | ✅ |
|
||||
| Game modes | Player's current game mode | ✅ |
|
||||
| Flight status | If the player is currently flying / can fly | ✅ |
|
||||
| Statistics | Player's in-game stats (ESC -> Statistics) | ✅ |
|
||||
| Location | Player's current coordinate position and world† | ✅ |
|
||||
| Persistent Data Container | Custom plugin persistent data key map | ✅️ |
|
||||
| Locked maps | Maps/treasure maps locked in a cartography table | ✅ |
|
||||
| Unlocked maps | Regular, unlocked maps/treasure maps ([why?](#map-syncing)) | ❌ |
|
||||
| Economy balances | Vault economy balance. ([why?](#economy-syncing)) | ❌ |
|
||||
|
||||
What about modded items? Or custom item plugins such as MMOItems or SlimeFun? These items are **not compatible**—check the [[FAQs]] for more information.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user