From 0124b6691829b67689ef6f3d58c6224ef453bf7d Mon Sep 17 00:00:00 2001 From: Outfluencer Date: Sat, 20 Sep 2025 10:33:18 +1000 Subject: [PATCH] #3874: Add ObjectComponents --- .../md_5/bungee/api/chat/ObjectComponent.java | 73 +++++++++ .../bungee/api/chat/objects/ChatObject.java | 5 + .../bungee/api/chat/objects/PlayerObject.java | 39 +++++ .../bungee/api/chat/objects/SpriteObject.java | 21 +++ .../md_5/bungee/api/chat/player/Profile.java | 40 +++++ .../md_5/bungee/api/chat/player/Property.java | 22 +++ .../chat/ObjectComponentSerializer.java | 138 ++++++++++++++++++ .../chat/VersionedComponentSerializer.java | 6 + 8 files changed, 344 insertions(+) create mode 100644 chat/src/main/java/net/md_5/bungee/api/chat/ObjectComponent.java create mode 100644 chat/src/main/java/net/md_5/bungee/api/chat/objects/ChatObject.java create mode 100644 chat/src/main/java/net/md_5/bungee/api/chat/objects/PlayerObject.java create mode 100644 chat/src/main/java/net/md_5/bungee/api/chat/objects/SpriteObject.java create mode 100644 chat/src/main/java/net/md_5/bungee/api/chat/player/Profile.java create mode 100644 chat/src/main/java/net/md_5/bungee/api/chat/player/Property.java create mode 100644 serializer/src/main/java/net/md_5/bungee/chat/ObjectComponentSerializer.java diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/ObjectComponent.java b/chat/src/main/java/net/md_5/bungee/api/chat/ObjectComponent.java new file mode 100644 index 00000000..2d619ff1 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/ObjectComponent.java @@ -0,0 +1,73 @@ +package net.md_5.bungee.api.chat; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; +import lombok.ToString; +import net.md_5.bungee.api.chat.objects.ChatObject; + +/** + * An object component that can be used to display objects. + *

+ * It can either display a player's head or an object by a specific sprite and + * an atlas. + *

+ * Note: this was added in Minecraft 1.21.9. + */ +@Getter +@Setter +@ToString +@EqualsAndHashCode(callSuper = true) +public final class ObjectComponent extends BaseComponent +{ + + private ChatObject object; + + /** + * Creates a ObjectComponent from a given ChatObject. + * + * See {@link net.md_5.bungee.api.chat.objects.PlayerObject} and + * {@link net.md_5.bungee.api.chat.objects.SpriteObject}. + * + * @param object the ChatObject + */ + public ObjectComponent(@NonNull ChatObject object) + { + this.object = object; + } + + /** + * Creates an object component from the original to clone it. + * + * @param original the original for the new score component + */ + public ObjectComponent(ObjectComponent original) + { + super( original ); + setObject( original.object ); + } + + @Override + public ObjectComponent duplicate() + { + return new ObjectComponent( this ); + } + + @Override + protected void toPlainText(StringVisitor builder) + { + // I guess we cannot convert this to plain text + // builder.append( this.value ); + super.toPlainText( builder ); + } + + @Override + protected void toLegacyText(StringVisitor builder) + { + addFormat( builder ); + // Same here... + // builder.append( this.value ); + super.toLegacyText( builder ); + } +} diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/objects/ChatObject.java b/chat/src/main/java/net/md_5/bungee/api/chat/objects/ChatObject.java new file mode 100644 index 00000000..2a932e2a --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/objects/ChatObject.java @@ -0,0 +1,5 @@ +package net.md_5.bungee.api.chat.objects; + +public interface ChatObject +{ +} diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/objects/PlayerObject.java b/chat/src/main/java/net/md_5/bungee/api/chat/objects/PlayerObject.java new file mode 100644 index 00000000..d44739ff --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/objects/PlayerObject.java @@ -0,0 +1,39 @@ +package net.md_5.bungee.api.chat.objects; + +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; +import net.md_5.bungee.api.chat.player.Profile; +import net.md_5.bungee.api.chat.player.Property; + +@Data +@AllArgsConstructor +public final class PlayerObject implements ChatObject +{ + + /** + * The profile of the player. + */ + @NonNull + private Profile profile; + /** + * If true, a hat layer will be rendered on the head. (default: true) + */ + private Boolean hat; + + public PlayerObject(@NonNull String name) + { + this.profile = new Profile( name ); + } + + public PlayerObject(@NonNull UUID uuid) + { + this.profile = new Profile( uuid ); + } + + public PlayerObject(@NonNull Property[] properties) + { + this.profile = new Profile( properties ); + } +} diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/objects/SpriteObject.java b/chat/src/main/java/net/md_5/bungee/api/chat/objects/SpriteObject.java new file mode 100644 index 00000000..7a1b1385 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/objects/SpriteObject.java @@ -0,0 +1,21 @@ +package net.md_5.bungee.api.chat.objects; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; + +@Data +@AllArgsConstructor +public final class SpriteObject implements ChatObject +{ + + /** + * The namespaced ID of a sprite atlas, default value: minecraft:blocks. + */ + private String atlas; + /** + * The namespaced ID of a sprite in atlas, for example item/porkchop. + */ + @NonNull + private String sprite; +} diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/player/Profile.java b/chat/src/main/java/net/md_5/bungee/api/chat/player/Profile.java new file mode 100644 index 00000000..84865c0b --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/player/Profile.java @@ -0,0 +1,40 @@ +package net.md_5.bungee.api.chat.player; + +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; + +@Data +@AllArgsConstructor +public class Profile +{ + + /** + * The name of the profile. Can be null. + */ + private String name; + /** + * The UUID of the profile. Can be null. + */ + private UUID uuid; + /** + * The properties of the profile. Can be null. + */ + private Property[] properties; + + public Profile(@NonNull String name) + { + this( name, null, null ); + } + + public Profile(@NonNull UUID uuid) + { + this( null, uuid, null ); + } + + public Profile(@NonNull Property[] properties) + { + this( null, null, properties ); + } +} diff --git a/chat/src/main/java/net/md_5/bungee/api/chat/player/Property.java b/chat/src/main/java/net/md_5/bungee/api/chat/player/Property.java new file mode 100644 index 00000000..b6dd8ea6 --- /dev/null +++ b/chat/src/main/java/net/md_5/bungee/api/chat/player/Property.java @@ -0,0 +1,22 @@ +package net.md_5.bungee.api.chat.player; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; + +@Data +@AllArgsConstructor +public class Property +{ + + @NonNull + private String name; + @NonNull + private String value; + private String signature; + + public Property(@NonNull String name, @NonNull String value) + { + this( name, value, null ); + } +} diff --git a/serializer/src/main/java/net/md_5/bungee/chat/ObjectComponentSerializer.java b/serializer/src/main/java/net/md_5/bungee/chat/ObjectComponentSerializer.java new file mode 100644 index 00000000..3cf1a095 --- /dev/null +++ b/serializer/src/main/java/net/md_5/bungee/chat/ObjectComponentSerializer.java @@ -0,0 +1,138 @@ +package net.md_5.bungee.chat; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; +import java.util.UUID; +import net.md_5.bungee.api.chat.ObjectComponent; +import net.md_5.bungee.api.chat.objects.PlayerObject; +import net.md_5.bungee.api.chat.objects.SpriteObject; +import net.md_5.bungee.api.chat.player.Profile; +import net.md_5.bungee.api.chat.player.Property; + +public class ObjectComponentSerializer extends BaseComponentSerializer implements JsonSerializer, JsonDeserializer +{ + + public ObjectComponentSerializer(VersionedComponentSerializer serializer) + { + super( serializer ); + } + + @Override + public ObjectComponent deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + JsonObject object = json.getAsJsonObject(); + + String sprite = object.has( "sprite" ) ? object.get( "sprite" ).getAsString() : null; + String atlas = object.has( "atlas" ) ? object.get( "atlas" ).getAsString() : null; + + if ( sprite != null ) + { + ObjectComponent component = new ObjectComponent( new SpriteObject( atlas, sprite ) ); + deserialize( object, component, context ); + return component; + } + JsonElement player = object.get( "player" ); + if ( player != null ) + { + String name = null; + UUID uuid = null; + Property[] properties = null; + Boolean hat = object.has( "hat" ) ? object.get( "hat" ).getAsBoolean() : null; + if ( player.isJsonObject() ) + { + JsonObject playerObj = player.getAsJsonObject(); + validateName( name = playerObj.has( "name" ) ? playerObj.get( "name" ).getAsString() : null ); + uuid = playerObj.has( "id" ) ? parseUUID( context.deserialize( playerObj.get( "id" ), int[].class ) ) : null; + properties = playerObj.has( "properties" ) ? context.deserialize( playerObj.get( "properties" ), Property[].class ) : null; + } else if ( player.isJsonPrimitive() ) + { + validateName( name = player.getAsString() ); + } + ObjectComponent component = new ObjectComponent( new PlayerObject( new Profile( name, uuid, properties ), hat ) ); + deserialize( object, component, context ); + return component; + } + + throw new JsonParseException( "Could not parse JSON: missing 'player' or 'sprite' property" ); + } + + @Override + public JsonElement serialize(ObjectComponent src, Type typeOfSrc, JsonSerializationContext context) + { + JsonObject object = new JsonObject(); + serialize( object, src, context ); + + if ( src.getObject() instanceof SpriteObject ) + { + SpriteObject sprite = (SpriteObject) src.getObject(); + object.addProperty( "sprite", sprite.getSprite() ); + if ( sprite.getAtlas() != null ) + { + object.addProperty( "atlas", sprite.getAtlas() ); + } + return object; + } + + if ( src.getObject() instanceof PlayerObject ) + { + PlayerObject player = (PlayerObject) src.getObject(); + + if ( player.getHat() != null ) + { + object.addProperty( "hat", player.getHat() ); + } + + JsonObject playerObj = new JsonObject(); + Profile profile = player.getProfile(); + + if ( profile.getName() != null ) + { + playerObj.addProperty( "name", profile.getName() ); + } + + if ( profile.getUuid() != null ) + { + int[] uuidArray = new int[4]; + long most = profile.getUuid().getMostSignificantBits(); + long least = profile.getUuid().getLeastSignificantBits(); + uuidArray[0] = (int) ( most >> 32 ); + uuidArray[1] = (int) most; + uuidArray[2] = (int) ( least >> 32 ); + uuidArray[3] = (int) least; + playerObj.add( "id", context.serialize( uuidArray ) ); + } + + if ( profile.getProperties() != null ) + { + playerObj.add( "properties", context.serialize( profile.getProperties(), Property[].class ) ); + } + object.add( "player", playerObj ); + return object; + } + + throw new JsonParseException( "Could not serialize ObjectComponent: unknown object type " + src.getObject().getClass() ); + } + + private static UUID parseUUID(int[] array) + { + if ( array.length != 4 ) + { + throw new JsonParseException( "UUID integer array must be exactly 4 integers long" ); + } + return new UUID( (long) array[0] << 32 | (long) array[1] & 0XFFFFFFFFL, (long) array[2] << 32 | (long) array[3] & 0XFFFFFFFFL ); + } + + private static void validateName(String name) + { + if ( name != null && ( name.length() > 16 || name.isEmpty() ) ) + { + throw new JsonParseException( "Could not parse JSON: player name must be 16 characters or fewer and not empty" ); + } + } +} diff --git a/serializer/src/main/java/net/md_5/bungee/chat/VersionedComponentSerializer.java b/serializer/src/main/java/net/md_5/bungee/chat/VersionedComponentSerializer.java index 60ac3fe3..43330050 100644 --- a/serializer/src/main/java/net/md_5/bungee/chat/VersionedComponentSerializer.java +++ b/serializer/src/main/java/net/md_5/bungee/chat/VersionedComponentSerializer.java @@ -17,6 +17,7 @@ import net.md_5.bungee.api.chat.BaseComponent; import net.md_5.bungee.api.chat.ComponentStyle; import net.md_5.bungee.api.chat.ItemTag; import net.md_5.bungee.api.chat.KeybindComponent; +import net.md_5.bungee.api.chat.ObjectComponent; import net.md_5.bungee.api.chat.ScoreComponent; import net.md_5.bungee.api.chat.SelectorComponent; import net.md_5.bungee.api.chat.TextComponent; @@ -61,6 +62,7 @@ public class VersionedComponentSerializer implements JsonDeserializer