diff --git a/api/src/main/java/net/kyori/adventure/audience/Audience.java b/api/src/main/java/net/kyori/adventure/audience/Audience.java index 16a56ee56e..587371fb07 100644 --- a/api/src/main/java/net/kyori/adventure/audience/Audience.java +++ b/api/src/main/java/net/kyori/adventure/audience/Audience.java @@ -37,6 +37,7 @@ import net.kyori.adventure.bossbar.BossBarViewer; import net.kyori.adventure.chat.ChatType; import net.kyori.adventure.chat.SignedMessage; +import net.kyori.adventure.dialog.DialogLike; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.inventory.Book; @@ -659,8 +660,6 @@ default void playSound(final @NotNull Sound sound, final double x, final double * *
To play a sound that follows the recipient, use {@link Sound.Emitter#self()}.
* - *Note: Due to MC-138832, the volume and pitch may be ignored when using this method.
- * * @param sound a sound * @param emitter an emitter * @since 4.8.0 @@ -862,4 +861,33 @@ default void removeResourcePacks(final @NotNull UUID id, final @NotNull UUID@Not */ default void clearResourcePacks() { } + + // ----------------- + // ---- Dialogs ---- + // ----------------- + + /** + * Shows a dialog to this audience. + * + *This method exists to allow initial native support for dialogs until Adventure + * has full API to support building and sending dialogs.
+ * + * @param dialog the dialog + * @since 4.22.0 + * @sinceMinecraft 1.21.6 + */ + default void showDialog(final @NotNull DialogLike dialog) { + } + + /** + * Closes the dialog that is currently being shown to this audience, if any. + * + *This will return the user to the previous dialog if one was opened from the + * current dialog.
+ * + * @since 4.24.0 + * @sinceMinecraft 1.21.6 + */ + default void closeDialog() { + } } diff --git a/api/src/main/java/net/kyori/adventure/audience/ForwardingAudience.java b/api/src/main/java/net/kyori/adventure/audience/ForwardingAudience.java index 197b5d4a9d..1771910768 100644 --- a/api/src/main/java/net/kyori/adventure/audience/ForwardingAudience.java +++ b/api/src/main/java/net/kyori/adventure/audience/ForwardingAudience.java @@ -34,6 +34,7 @@ import net.kyori.adventure.bossbar.BossBar; import net.kyori.adventure.chat.ChatType; import net.kyori.adventure.chat.SignedMessage; +import net.kyori.adventure.dialog.DialogLike; import net.kyori.adventure.identity.Identified; import net.kyori.adventure.identity.Identity; import net.kyori.adventure.inventory.Book; @@ -221,6 +222,16 @@ default void clearResourcePacks() { for (final Audience audience : this.audiences()) audience.clearResourcePacks(); } + @Override + default void showDialog(final @NotNull DialogLike dialog) { + for (final Audience audience : this.audiences()) audience.showDialog(dialog); + } + + @Override + default void closeDialog() { + for (final Audience audience : this.audiences()) audience.closeDialog(); + } + /** * An audience that forwards everything to a single other audience. * @@ -403,5 +414,15 @@ default void removeResourcePacks(final @NotNull UUID id, final @NotNull UUID @No default void clearResourcePacks() { this.audience().clearResourcePacks(); } + + @Override + default void showDialog(final @NotNull DialogLike dialog) { + this.audience().showDialog(dialog); + } + + @Override + default void closeDialog() { + this.audience().closeDialog(); + } } } diff --git a/api/src/main/java/net/kyori/adventure/bossbar/BossBarImpl.java b/api/src/main/java/net/kyori/adventure/bossbar/BossBarImpl.java index b88c3df004..410b5f376f 100644 --- a/api/src/main/java/net/kyori/adventure/bossbar/BossBarImpl.java +++ b/api/src/main/java/net/kyori/adventure/bossbar/BossBarImpl.java @@ -26,7 +26,6 @@ import java.util.Collections; import java.util.EnumSet; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; @@ -92,12 +91,12 @@ private ImplementationAccessor() { @Override public @NotNull BossBar name(final @NotNull Component newName) { + // We do not check if the new name equals the old name here as the GlobalTranslator + // may produce a different resulting component for the end user. requireNonNull(newName, "name"); final Component oldName = this.name; - if (!Objects.equals(newName, oldName)) { - this.name = newName; - this.forEachListener(listener -> listener.bossBarNameChanged(this, oldName, newName)); - } + this.name = newName; + this.forEachListener(listener -> listener.bossBarNameChanged(this, oldName, newName)); return this; } diff --git a/api/src/main/java/net/kyori/adventure/dialog/DialogLike.java b/api/src/main/java/net/kyori/adventure/dialog/DialogLike.java new file mode 100644 index 0000000000..a36e7b653b --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/dialog/DialogLike.java @@ -0,0 +1,39 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.dialog; + +import net.kyori.adventure.audience.Audience; + +/** + * Something that can be represented as a Dialog. + * + *This interface exists to allow initial native support for dialogs until Adventure + * has full API to support building and sending dialogs.
+ * + * @see Audience#showDialog(DialogLike) + * @since 4.22.0 + * @sinceMinecraft 1.21.6 + */ +public interface DialogLike { +} diff --git a/api/src/main/java/net/kyori/adventure/dialog/package-info.java b/api/src/main/java/net/kyori/adventure/dialog/package-info.java new file mode 100644 index 0000000000..8887ed2226 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/dialog/package-info.java @@ -0,0 +1,30 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +/** + * Dialogs. + * + * @sinceMinecraft 1.21.6 + * @since 4.22 + */ +package net.kyori.adventure.dialog; diff --git a/api/src/main/java/net/kyori/adventure/pointer/PointersSupplierImpl.java b/api/src/main/java/net/kyori/adventure/pointer/PointersSupplierImpl.java index 3d1181b622..e1f01d24d7 100644 --- a/api/src/main/java/net/kyori/adventure/pointer/PointersSupplierImpl.java +++ b/api/src/main/java/net/kyori/adventure/pointer/PointersSupplierImpl.java @@ -33,7 +33,7 @@ import org.jetbrains.annotations.Nullable; final class PointersSupplierImplThere are some bugs that are of note when using sounds:
+ *The documentation for each source details the vanilla use.
+ * * @since 4.0.0 */ enum Source { + /** + * The main sound source. + * + *This source controls the overall sound of the game.
+ * + * @since 4.0.0 + */ MASTER("master"), + + /** + * The music sound source. + * + *This source handles the in-game soundtrack.
+ * + * @since 4.0.0 + */ MUSIC("music"), + + /** + * The record sound source. + * + *This source handles jukeboxes and note blocks.
+ * + * @since 4.0.0 + */ RECORD("record"), + + /** + * The weather sound source. + * + *This source handles weather sounds.
+ * + * @since 4.0.0 + */ WEATHER("weather"), + + /** + * The block sound source. + * + *This source handles player interaction with blocks as well as passive block sounds.
+ * + * @since 4.0.0 + */ BLOCK("block"), + + /** + * The hostile sound source. + * + *This source handles hostile entities.
+ * + * @since 4.0.0 + */ HOSTILE("hostile"), + + /** + * The neutral sound source. + * + *This source handles neutral entities.
+ * + * @since 4.0.0 + */ NEUTRAL("neutral"), + + /** + * The player sound source. + * + *This source handles player entities.
+ * + * @since 4.0.0 + */ PLAYER("player"), + + /** + * The ambient sound source. + * + *This source handles ambience.
+ * + * @since 4.0.0 + */ AMBIENT("ambient"), - VOICE("voice"); + + /** + * The voice sound source. + * + *This source handles the narrator.
+ * + * @since 4.0.0 + */ + VOICE("voice"), + + /** + * The UI sound source. + * + *This source handles UI actions.
+ * + * @since 4.22.0 + * @sinceMinecraft 1.21.6 + */ + UI("ui"); /** * The name map. diff --git a/api/src/main/java/net/kyori/adventure/sound/SoundImpl.java b/api/src/main/java/net/kyori/adventure/sound/SoundImpl.java index 64ac45823f..ba05709e00 100644 --- a/api/src/main/java/net/kyori/adventure/sound/SoundImpl.java +++ b/api/src/main/java/net/kyori/adventure/sound/SoundImpl.java @@ -73,7 +73,7 @@ public float pitch() { } @Override - public OptionalLong seed() { + public @NotNull OptionalLong seed() { return this.seed; } diff --git a/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java b/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java index b4d7964c66..300ab50e65 100644 --- a/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java +++ b/api/src/main/java/net/kyori/adventure/text/AbstractComponentBuilder.java @@ -289,7 +289,9 @@ private void prepareChildren() { @Override @SuppressWarnings("unchecked") public @NotNull B mergeStyle(final @NotNull Component that, final @NotNull SetSee {@link BinaryTagHolder#binaryTagHolder(String)} for a simple way to create NBT from SNBT. + * For simple use cases, you can use plain strings directly as SNBT.
+ * + * @param key the key identifying the payload + * @param nbt the nbt data + * @return the click event + * @since 4.23.0 + */ + public static @NotNull ClickEvent custom(final @NotNull Key key, final @NotNull BinaryTagHolder nbt) { + requireNonNull(key, "key"); + requireNonNull(nbt, "nbt"); + return new ClickEvent(Action.CUSTOM, Payload.custom(key, nbt)); + } + + /** + * Creates a click event with a {@link Payload.Text string payload}. * * @param action the action * @param value the value * @return a click event + * @throws IllegalArgumentException if the action does not support a string payload * @since 4.0.0 + * @deprecated For removal since 4.22.0, not all actions support string payloads */ + @Deprecated public static @NotNull ClickEvent clickEvent(final @NotNull Action action, final @NotNull String value) { - return new ClickEvent(action, value); + // A special case here to ensure that page can still accept a string. + if (action == Action.CHANGE_PAGE) return changePage(value); + if (!action.payloadType().equals(Payload.Text.class)) throw new IllegalArgumentException("Action " + action + " does not support string payloads"); + return new ClickEvent(action, Payload.string(value)); } private final Action action; - private final String value; + private final Payload payload; - private ClickEvent(final @NotNull Action action, final @NotNull String value) { + private ClickEvent(final @NotNull Action action, final @NotNull Payload payload) { + if (!action.supports(payload)) throw new IllegalArgumentException("Action " + action + " does not support payload " + payload); this.action = requireNonNull(action, "action"); - this.value = requireNonNull(value, "value"); + this.payload = requireNonNull(payload, "payload"); } /** @@ -210,13 +269,32 @@ private ClickEvent(final @NotNull Action action, final @NotNull String value) { } /** - * Gets the click event value. + * Gets the click event value if the payload is a {@link Payload.Text string payload}. * * @return the click event value + * @throws IllegalStateException if the payload is not a string payload * @since 4.0.0 + * @deprecated For removal since 4.22.0, click events can hold more than just strings, see {@link #payload()} */ + @Deprecated public @NotNull String value() { - return this.value; + if (this.payload instanceof Payload.Text) { + return ((Payload.Text) this.payload).value(); + } else if (this.action == Action.CHANGE_PAGE) { // Special case for page. + return String.valueOf(((Payload.Int) this.payload).integer()); + } else { + throw new IllegalStateException("Payload is not a string payload, is " + this.payload); + } + } + + /** + * Gets the payload associated with this click event. + * + * @return the payload + * @since 4.22.0 + */ + public @NotNull Payload payload() { + return this.payload; } @Override @@ -229,13 +307,13 @@ public boolean equals(final @Nullable Object other) { if (this == other) return true; if (other == null || this.getClass() != other.getClass()) return false; final ClickEvent that = (ClickEvent) other; - return this.action == that.action && Objects.equals(this.value, that.value); + return this.action == that.action && Objects.equals(this.payload, that.payload); } @Override public int hashCode() { int result = this.action.hashCode(); - result = (31 * result) + this.value.hashCode(); + result = (31 * result) + this.payload.hashCode(); return result; } @@ -243,7 +321,7 @@ public int hashCode() { public @NotNull Stream extends ExaminableProperty> examinableProperties() { return Stream.of( ExaminableProperty.of("action", this.action), - ExaminableProperty.of("value", this.value) + ExaminableProperty.of("payload", this.payload) ); } @@ -252,6 +330,155 @@ public String toString() { return Internals.toString(this); } + /** + * A payload for a click event. + * + * @since 4.22.0 + */ + public /* sealed */ interface Payload /* permits String, Dialog, Custom */ extends Examinable { + /** + * Creates a text payload. + * + * @param value the payload value + * @return the payload + * @since 4.22.0 + */ + static ClickEvent.Payload.@NotNull Text string(final @NotNull String value) { + requireNonNull(value, "value"); + return new PayloadImpl.TextImpl(value); + } + + /** + * Creates an integer payload. + * + * @param integer the integer + * @return the payload + * @since 4.22.0 + */ + static ClickEvent.Payload.@NotNull Int integer(final int integer) { + return new PayloadImpl.IntImpl(integer); + } + + /** + * Creates a dialog payload. + * + * @param dialog the payload value + * @return the payload + * @since 4.22.0 + */ + static Payload.@NotNull Dialog dialog(final @NotNull DialogLike dialog) { + requireNonNull(dialog, "dialog"); + return new PayloadImpl.DialogImpl(dialog); + } + + /** + * Creates a custom payload. + * + * @param key the key identifying the payload + * @param data the payload data + * @return the payload + * @since 4.22.0 + * @deprecated For removal since 4.23.0, payloads hold NBT data, use {@link #custom(Key, BinaryTagHolder)} instead. + * This method will create NBT using {@link BinaryTagHolder#binaryTagHolder(String)}. + */ + @Deprecated + static Payload.@NotNull Custom custom(final @NotNull Key key, final @NotNull String data) { + return Payload.custom(key, BinaryTagHolder.binaryTagHolder(data)); + } + + /** + * Creates a custom payload. + * + *See {@link BinaryTagHolder#binaryTagHolder(String)} for a simple way to create NBT from SNBT. + * For simple use cases, you can use plain strings directly as SNBT.
+ * + * @param key the key identifying the payload + * @param nbt the payload nbt data + * @return the payload + * @since 4.23.0 + */ + static Payload.@NotNull Custom custom(final @NotNull Key key, final @NotNull BinaryTagHolder nbt) { + requireNonNull(key, "key"); + requireNonNull(nbt, "nbt"); + return new PayloadImpl.CustomImpl(key, nbt); + } + + /** + * A payload that holds a string. + * + * @since 4.22.0 + */ + interface Text extends Payload { + /** + * The string value for this payload. + * + * @return the string + * @since 4.22.0 + */ + @NotNull String value(); + } + + /** + * A payload that holds an integer. + * + * @since 4.22.0 + */ + interface Int extends Payload { + /** + * The integer value for this payload. + * + * @return the integer + * @since 4.22.0 + */ + int integer(); + } + + /** + * A payload that holds a dialog. + * + * @see Action#SHOW_DIALOG + * @since 4.22.0 + */ + interface Dialog extends Payload { + /** + * The dialog. + * + * @return the dialog + * @since 4.22.0 + */ + @NotNull DialogLike dialog(); + } + + /** + * A payload that holds custom data. + * + * @see Action#CUSTOM + * @since 4.22.0 + */ + interface Custom extends Payload, Keyed { + /** + * The custom data. + * + * @return the data + * @since 4.22.0 + * @deprecated For removal since 4.23.0, custom payloads contain NBT data, use {@link #nbt()} instead. + * This method will return {@link BinaryTagHolder#string()} on the held NBT. + */ + @Deprecated + @NotNull String data(); + + /** + * The custom data. + * + *See {@link BinaryTagHolder#string()} for a simple way to return SNBT from NBT data.
+ * + * @return the data + * @since 4.23.0 + */ + @NotNull BinaryTagHolder nbt(); + } + } + /** * An enumeration of click event actions. * @@ -263,7 +490,7 @@ public enum Action { * * @since 4.0.0 */ - OPEN_URL("open_url", true), + OPEN_URL("open_url", true, Payload.Text.class), /** * Opens a file when clicked. * @@ -271,32 +498,48 @@ public enum Action { * * @since 4.0.0 */ - OPEN_FILE("open_file", false), + OPEN_FILE("open_file", false, Payload.Text.class), /** * Runs a command when clicked. * * @since 4.0.0 */ - RUN_COMMAND("run_command", true), + RUN_COMMAND("run_command", true, Payload.Text.class), /** * Suggests a command into the chat box. * * @since 4.0.0 */ - SUGGEST_COMMAND("suggest_command", true), + SUGGEST_COMMAND("suggest_command", true, Payload.Text.class), /** * Changes the page of a book. * * @since 4.0.0 */ - CHANGE_PAGE("change_page", true), + CHANGE_PAGE("change_page", true, Payload.Int.class), /** * Copies text to the clipboard. * * @since 4.0.0 * @sinceMinecraft 1.15 */ - COPY_TO_CLIPBOARD("copy_to_clipboard", true); + COPY_TO_CLIPBOARD("copy_to_clipboard", true, Payload.Text.class), + /** + * Shows a dialog. + * + *This action is not readable at this time until Adventure has a full Dialog API.
+ * + * @since 4.22.0 + * @sinceMinecraft 1.21.6 + */ + SHOW_DIALOG("show_dialog", false, Payload.Dialog.class), + /** + * Sends a custom event to the server. + * + * @since 4.22.0 + * @sinceMinecraft 1.21.6 + */ + CUSTOM("custom", true, Payload.Custom.class); /** * The name map. @@ -311,10 +554,12 @@ public enum Action { *When an action is not readable it will not be deserialized.
*/ private final boolean readable; + private final Class extends Payload> payloadType; - Action(final @NotNull String name, final boolean readable) { + Action(final @NotNull String name, final boolean readable, final @NotNull Class extends Payload> payloadType) { this.name = name; this.readable = readable; + this.payloadType = payloadType; } /** @@ -328,6 +573,28 @@ public boolean readable() { return this.readable; } + /** + * Returns if this action supports the provided payload. + * + * @param payload the payload + * @return {@code true} if this action supports the payload + * @since 4.22.0 + */ + public boolean supports(final @NotNull Payload payload) { + requireNonNull(payload, "payload"); + return this.payloadType.isAssignableFrom(payload.getClass()); + } + + /** + * The type of the payload this click event supports. + * + * @return the payload type + * @since 4.22.0 + */ + public @NotNull Class extends Payload> payloadType() { + return this.payloadType; + } + @Override public @NotNull String toString() { return this.name; diff --git a/api/src/main/java/net/kyori/adventure/text/event/PayloadImpl.java b/api/src/main/java/net/kyori/adventure/text/event/PayloadImpl.java new file mode 100644 index 0000000000..4d6b679704 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/event/PayloadImpl.java @@ -0,0 +1,187 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text.event; + +import java.util.Objects; +import java.util.stream.Stream; +import net.kyori.adventure.dialog.DialogLike; +import net.kyori.adventure.internal.Internals; +import net.kyori.adventure.key.Key; +import net.kyori.adventure.nbt.api.BinaryTagHolder; +import net.kyori.examination.ExaminableProperty; +import org.jetbrains.annotations.NotNull; + +abstract class PayloadImpl implements ClickEvent.Payload { + @Override + public String toString() { + return Internals.toString(this); + } + + static final class TextImpl extends PayloadImpl implements ClickEvent.Payload.Text { + private final String value; + + TextImpl(final @NotNull String value) { + this.value = value; + } + + @Override + public @NotNull String value() { + return this.value; + } + + @Override + public @NotNull Stream extends ExaminableProperty> examinableProperties() { + return Stream.of( + ExaminableProperty.of("value", this.value) + ); + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + final TextImpl that = (TextImpl) other; + return Objects.equals(this.value, that.value); + } + + @Override + public int hashCode() { + return this.value.hashCode(); + } + } + + static final class IntImpl extends PayloadImpl implements ClickEvent.Payload.Int { + private final int integer; + + IntImpl(final int integer) { + this.integer = integer; + } + + @Override + public int integer() { + return this.integer; + } + + @Override + public @NotNull Stream extends ExaminableProperty> examinableProperties() { + return Stream.of( + ExaminableProperty.of("integer", this.integer) + ); + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + final IntImpl that = (IntImpl) other; + return Objects.equals(this.integer, that.integer); + } + + @Override + public int hashCode() { + return this.integer; + } + } + + static final class DialogImpl extends PayloadImpl implements ClickEvent.Payload.Dialog { + private final DialogLike dialogLike; + + DialogImpl(final @NotNull DialogLike dialogLike) { + this.dialogLike = dialogLike; + } + + @Override + public @NotNull DialogLike dialog() { + return this.dialogLike; + } + + @Override + public @NotNull Stream extends ExaminableProperty> examinableProperties() { + return Stream.of( + ExaminableProperty.of("dialog", this.dialogLike) + ); + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + final DialogImpl that = (DialogImpl) other; + return Objects.equals(this.dialogLike, that.dialogLike); + } + + @Override + public int hashCode() { + return this.dialogLike.hashCode(); + } + } + + static final class CustomImpl extends PayloadImpl implements ClickEvent.Payload.Custom { + private final Key key; + private final BinaryTagHolder nbt; + + CustomImpl(final @NotNull Key key, final @NotNull BinaryTagHolder nbt) { + this.key = key; + this.nbt = nbt; + } + + @Override + public @NotNull Key key() { + return this.key; + } + + @Override + public @NotNull String data() { + return this.nbt.string(); + } + + @Override + public @NotNull BinaryTagHolder nbt() { + return this.nbt; + } + + @Override + public @NotNull Stream extends ExaminableProperty> examinableProperties() { + return Stream.of( + ExaminableProperty.of("key", this.key), + ExaminableProperty.of("nbt", this.nbt) + ); + } + + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (other == null || getClass() != other.getClass()) return false; + final CustomImpl that = (CustomImpl) other; + return Objects.equals(this.key, that.key) && Objects.equals(this.nbt, that.nbt); + } + + @Override + public int hashCode() { + int result = this.key.hashCode(); + result = (31 * result) + this.nbt.hashCode(); + return result; + } + } +} diff --git a/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattener.java b/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattener.java index 0186d28993..f863a567e7 100644 --- a/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattener.java +++ b/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattener.java @@ -31,6 +31,7 @@ import net.kyori.adventure.util.Buildable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; /** * A 'flattener' to convert a component tree to a linear string for display. @@ -38,6 +39,13 @@ * @since 4.7.0 */ public interface ComponentFlattener extends BuildableThe default value is {@link #NO_NESTING_LIMIT}, which means there is no limit on nesting.
+ * + * @param limit the new limit (must be a positive integer, or {@link #NO_NESTING_LIMIT}) + * @return this builder + * @since 4.22.0 + */ + @NotNull Builder nestingLimit(final @Range(from = 1, to = Integer.MAX_VALUE) int limit); } } diff --git a/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java b/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java index 7f00c20819..bcbf07252d 100644 --- a/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java +++ b/api/src/main/java/net/kyori/adventure/text/flattener/ComponentFlattenerImpl.java @@ -23,6 +23,9 @@ */ package net.kyori.adventure.text.flattener; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; @@ -36,6 +39,7 @@ import net.kyori.adventure.util.InheritanceAwareMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; import static java.util.Objects.requireNonNull; @@ -64,41 +68,90 @@ final class ComponentFlattenerImpl implements ComponentFlattener { private final InheritanceAwareMap