Skip to content

Commit

Permalink
Add attribute system for characters
Browse files Browse the repository at this point in the history
Introduces a new attribute system, including `Attribute`, `AttributeType`, `AttributeProvider`, and their implementation in `PaperAttributeProvider`. Attributes can now be registered, retrieved, and serialized for characters, enhancing extensibility and functionality within the system.
  • Loading branch information
NonSwag committed Jan 25, 2025
1 parent 5333b94 commit 0ced753
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 6 deletions.
4 changes: 4 additions & 0 deletions api/src/main/java/net/thenextlvl/character/Character.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.thenextlvl.character.action.ClickAction;
import net.thenextlvl.character.attribute.Attribute;
import net.thenextlvl.character.attribute.AttributeType;
import net.thenextlvl.character.tag.TagOptions;
import org.bukkit.Location;
import org.bukkit.World;
Expand Down Expand Up @@ -63,6 +65,8 @@ public interface Character<T extends Entity> extends TagSerializable {
@Unmodifiable
Set<UUID> getViewers();

<V> Optional<Attribute<V>> getAttribute(AttributeType<V> type);

@Nullable
World getWorld();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.thenextlvl.character.attribute;

import core.nbt.serialization.TagSerializable;
import org.jspecify.annotations.NonNull;

public interface Attribute<T> extends TagSerializable {
@NonNull
AttributeType<T> getType();

T getValue();

boolean setValue(T value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.thenextlvl.character.attribute;

import net.thenextlvl.character.Character;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.Unmodifiable;
import org.jspecify.annotations.NullMarked;

import java.util.Set;
import java.util.function.BiConsumer;

@NullMarked
public interface AttributeProvider {
<V extends Entity, T> void register(
AttributeType<T> type, Class<V> target,
BiConsumer<Character<V>, T> setter
);

@Unmodifiable
Set<AttributeType<?>> getAttributeTypes();

boolean isRegistered(AttributeType<?> type);

boolean unregister(AttributeType<?> type);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.thenextlvl.character.attribute;

import org.jspecify.annotations.NonNull;

import java.util.Objects;

public class AttributeType<T> {
private final @NonNull Class<T> dataType;
private final @NonNull String name;

public AttributeType(@NonNull String name, @NonNull Class<T> dataType) {
this.dataType = dataType;
this.name = name;
}

public @NonNull Class<T> getDataType() {
return dataType;
}

public @NonNull String getName() {
return name;
}

@Override
public boolean equals(Object o) {
if (o == null || getClass() != o.getClass()) return false;
AttributeType<?> that = (AttributeType<?>) o;
return Objects.equals(dataType, that.dataType) && Objects.equals(name, that.name);
}

@Override
public int hashCode() {
return Objects.hash(dataType, name);
}

@Override
public String toString() {
return "AttributeType{" +
"dataType=" + dataType +
", name='" + name + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package net.thenextlvl.character.attribute;

import org.bukkit.entity.Pose;
import org.jspecify.annotations.NullMarked;

@NullMarked
public class AttributeTypes {
public static AttributeType<Pose> POSE = new AttributeType<>("pose", Pose.class);

public static class Player {
public static AttributeType<Boolean> SNEAKING = new AttributeType<>("pose", boolean.class);
}

public static class Allay {
public static AttributeType<Boolean> DANCING = new AttributeType<>("dancing", boolean.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@
import net.kyori.adventure.text.format.NamedTextColor;
import net.thenextlvl.character.Character;
import net.thenextlvl.character.action.ClickAction;
import net.thenextlvl.character.attribute.Attribute;
import net.thenextlvl.character.attribute.AttributeType;
import net.thenextlvl.character.plugin.CharacterPlugin;
import net.thenextlvl.character.plugin.model.EmptyLootTable;
import net.thenextlvl.character.tag.TagOptions;
import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.attribute.Attributable;
import org.bukkit.attribute.Attribute;
import org.bukkit.entity.Display.Billboard;
import org.bukkit.entity.Display.Brightness;
import org.bukkit.entity.Entity;
Expand Down Expand Up @@ -56,11 +57,13 @@
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
import static org.bukkit.attribute.Attribute.SCALE;

@NullMarked
public class PaperCharacter<T extends Entity> implements Character<T> {
protected final Equipment equipment = new PaperEquipment();
protected final Map<String, ClickAction<?>> actions = new LinkedHashMap<>();
protected final Set<Attribute<?>> attributes = new HashSet<>();
protected final Set<UUID> viewers = new HashSet<>();
protected final String scoreboardName = StringUtil.random(32);
protected final TagOptions tagOptions = new PaperTagOptions();
Expand Down Expand Up @@ -170,6 +173,19 @@ public EntityType getType() {
return Set.copyOf(viewers);
}

@Override
@SuppressWarnings("unchecked")
public <V> Optional<Attribute<V>> getAttribute(AttributeType<V> type) {
return attributes.stream()
.filter(attribute -> attribute.getType().equals(type))
.map(attribute -> (Attribute<V>) attribute)
.findAny().or(() -> {
var attribute = plugin.attributeProvider().createAttribute(type, this);
attribute.ifPresent(attributes::add);
return attribute;
});
}

@Override
public @Nullable World getWorld() {
return getEntity().map(Entity::getWorld).orElse(null);
Expand Down Expand Up @@ -424,7 +440,7 @@ public boolean setPose(Pose pose) {
@Override
public boolean setScale(double scale) {
if (scale == this.scale) return false;
getEntity(Attributable.class).map(instance -> instance.getAttribute(Attribute.SCALE))
getEntity(Attributable.class).map(instance -> instance.getAttribute(SCALE))
.ifPresent(attribute -> attribute.setBaseValue(scale));
this.scale = scale;
return true;
Expand Down Expand Up @@ -523,16 +539,22 @@ public CompoundTag serialize() throws ParserException {
tag.add("ticking", ticking);
tag.add("type", plugin.nbt().toTag(type));
tag.add("visibleByDefault", visibleByDefault);
var clickActions = new CompoundTag();
actions.forEach((name, clickAction) -> clickActions.add(name, plugin.nbt().toTag(clickAction)));
if (!clickActions.isEmpty()) tag.add("clickActions", clickActions);
var actions = new CompoundTag();
var attributes = new CompoundTag();
this.actions.forEach((name, clickAction) -> actions.add(name, plugin.nbt().toTag(clickAction)));
this.attributes.forEach(attribute -> attributes.add(attribute.getType().getName(), attribute.serialize()));
if (!actions.isEmpty()) tag.add("clickActions", actions);
if (!attributes.isEmpty()) tag.add("attributes", attributes);
return tag;
}

@Override
public void deserialize(Tag tag) throws ParserException {
var root = tag.getAsCompound();
root.optional("ai").map(Tag::getAsBoolean).ifPresent(this::setAI);
// todo: implement attribute loading
// root.optional("attributes").map(Tag::getAsCompound).ifPresent(attributes -> attributes.forEach((name, attribute) ->
// this.attributes.add(plugin.nbt().fromTag(attribute, Attribute.class))));
root.optional("clickActions").map(Tag::getAsCompound).ifPresent(actions -> actions.forEach((name, action) ->
addAction(name, plugin.nbt().fromTag(action, ClickAction.class))));
root.optional("collidable").map(Tag::getAsBoolean).ifPresent(this::setCollidable);
Expand All @@ -553,7 +575,7 @@ public void deserialize(Tag tag) throws ParserException {

protected void preSpawn(T entity) {
if (entity instanceof Attributable attributable) {
var scale = attributable.getAttribute(Attribute.SCALE);
var scale = attributable.getAttribute(SCALE);
if (scale != null) scale.setBaseValue(this.scale);
}
if (entity instanceof LivingEntity living) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package net.thenextlvl.character.plugin.character.attribute;

import core.nbt.serialization.ParserException;
import core.nbt.tag.Tag;
import net.thenextlvl.character.Character;
import net.thenextlvl.character.attribute.Attribute;
import net.thenextlvl.character.attribute.AttributeType;
import net.thenextlvl.character.plugin.CharacterPlugin;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NonNull;

import java.util.Objects;
import java.util.function.BiConsumer;

public class PaperAttribute<V extends Entity, T> implements Attribute<T> {
private final @NonNull AttributeType<T> type;
private final @NonNull BiConsumer<Character<@NotNull V>, T> onChange;
private final @NonNull Character<@NotNull V> character;
private final @NonNull CharacterPlugin plugin;
private T value;

public PaperAttribute(@NonNull AttributeType<T> type, @NonNull BiConsumer<Character<@NotNull V>, T> onChange,
@NonNull Character<@NotNull V> character, @NonNull CharacterPlugin plugin) {
this.type = type;
this.onChange = onChange;
this.character = character;
this.plugin = plugin;
}


@Override
public @NotNull AttributeType<T> getType() {
return type;
}

@Override
public T getValue() {
return value;
}

@Override
public boolean setValue(T value) {
if (Objects.equals(this.value, value)) return false;
onChange.accept(character, this.value = value);
return true;
}

@Override
public @NotNull Tag serialize() throws ParserException {
return plugin.nbt().toTag(value);
}

@Override
public void deserialize(@NotNull Tag tag) throws ParserException {
setValue(plugin.nbt().fromTag(tag, type.getDataType()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package net.thenextlvl.character.plugin.character.attribute;

import com.google.common.base.Preconditions;
import net.thenextlvl.character.Character;
import net.thenextlvl.character.attribute.Attribute;
import net.thenextlvl.character.attribute.AttributeProvider;
import net.thenextlvl.character.attribute.AttributeType;
import net.thenextlvl.character.plugin.CharacterPlugin;
import org.bukkit.entity.Entity;
import org.jetbrains.annotations.Unmodifiable;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;

@NullMarked
public class PaperAttributeProvider implements AttributeProvider {
private final Map<AttributeType<?>, Function<Character<?>, @Nullable Attribute<?>>> attributes = new HashMap<>();
private final CharacterPlugin plugin;

public PaperAttributeProvider(CharacterPlugin plugin) {
this.plugin = plugin;
}

@SuppressWarnings("unchecked")
public <T> Optional<Attribute<T>> createAttribute(AttributeType<T> type, Character<?> character) {
return Optional.ofNullable(attributes.get(type)).map(function ->
(Attribute<T>) function.apply(character));
}

@Override
@SuppressWarnings("unchecked")
public <V extends Entity, T> void register(
AttributeType<T> type, Class<V> target,
BiConsumer<Character<V>, T> onChange
) {
Preconditions.checkArgument(!isRegistered(type), "Attribute type already registered: %s", type);
attributes.put(type, character -> {
var entityClass = character.getType().getEntityClass();
if (entityClass == null || !target.isAssignableFrom(entityClass)) return null;
return new PaperAttribute<>(type, onChange, (Character<V>) character, plugin);
});
}

@Override
public @Unmodifiable Set<AttributeType<?>> getAttributeTypes() {
return Set.copyOf(attributes.keySet());
}

@Override
public boolean isRegistered(AttributeType<?> type) {
return attributes.containsKey(type);
}

@Override
public boolean unregister(AttributeType<?> type) {
return attributes.remove(type) != null;
}
}

0 comments on commit 0ced753

Please sign in to comment.