diff --git a/api/src/main/java/net/kyori/adventure/sound/Sound.java b/api/src/main/java/net/kyori/adventure/sound/Sound.java index 6021dfe92..c5e270348 100644 --- a/api/src/main/java/net/kyori/adventure/sound/Sound.java +++ b/api/src/main/java/net/kyori/adventure/sound/Sound.java @@ -23,13 +23,17 @@ */ package net.kyori.adventure.sound; +import java.util.OptionalLong; +import java.util.function.Consumer; import java.util.function.Supplier; +import net.kyori.adventure.builder.AbstractBuilder; import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Keyed; import net.kyori.adventure.util.Index; import net.kyori.examination.Examinable; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Range; import static java.util.Objects.requireNonNull; @@ -55,6 +59,38 @@ */ @ApiStatus.NonExtendable public interface Sound extends Examinable { + /** + * Create a new builder for {@link Sound} instances. + * + * @return a new builder + * @since 4.12.0 + */ + static @NotNull Builder sound() { + return new SoundImpl.BuilderImpl(); + } + + /** + * Create a new builder for {@link Sound} instances. + * + * @param existing an existing sound to populate the builder with + * @return a new builder + * @since 4.12.0 + */ + static @NotNull Builder sound(final @NotNull Sound existing) { + return new SoundImpl.BuilderImpl(existing); + } + + /** + * Create a new {@link Sound} instance configured by the provided function. + * + * @param configurer a function that configures a builder + * @return a new builder + * @since 4.12.0 + */ + static @NotNull Sound sound(final @NotNull Consumer configurer) { + return AbstractBuilder.configureAndBuild(sound(), configurer); + } + /** * Creates a new sound. * @@ -66,14 +102,7 @@ public interface Sound extends Examinable { * @since 4.0.0 */ static @NotNull Sound sound(final @NotNull Key name, final @NotNull Source source, final float volume, final float pitch) { - requireNonNull(name, "name"); - requireNonNull(source, "source"); - return new SoundImpl(source, volume, pitch) { - @Override - public @NotNull Key name() { - return name; - } - }; + return sound().type(name).source(source).volume(volume).pitch(pitch).build(); } /** @@ -102,14 +131,7 @@ public interface Sound extends Examinable { * @since 4.0.0 */ static @NotNull Sound sound(final @NotNull Supplier type, final @NotNull Source source, final float volume, final float pitch) { - requireNonNull(type, "type"); - requireNonNull(source, "source"); - return new SoundImpl(source, volume, pitch) { - @Override - public @NotNull Key name() { - return type.get().key(); - } - }; + return sound().type(type).source(source).volume(volume).pitch(pitch).build(); } /** @@ -186,6 +208,16 @@ public interface Sound extends Examinable { */ float pitch(); + /** + * Get the seed used for playback of weighted sound effects. + * + *

When the seed is not provided, the seed of the receiver's world will be used instead.

+ * + * @return the seed to use + * @since 4.12.0 + */ + @NotNull OptionalLong seed(); + /** * Gets the {@link SoundStop} that will stop this specific sound. * @@ -274,4 +306,112 @@ interface Emitter { return SoundImpl.EMITTER_SELF; } } + + /** + * A builder for sound instances. + * + *

Type is required, all other options are optional.

+ * + * @since 4.12.0 + */ + interface Builder extends AbstractBuilder { + /** + * Set the type of this sound. + * + *

Required.

+ * + * @param type resource location of the sound event to play + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder type(final @NotNull Key type); + + /** + * Set the type of this sound. + * + *

Required.

+ * + * @param type a type of sound to play + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder type(final @NotNull Type type); + + /** + * Set the type of this sound. + * + *

Required.

+ * + * @param typeSupplier a type of sound to play, evaluated lazily + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder type(final @NotNull Supplier typeSupplier); + + /** + * A {@link Source} to tell the game where the sound is coming from. + * + *

By default, {@link Source#MASTER} is used.

+ * + * @param source a source + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder source(final @NotNull Source source); + + /** + * A {@link Source} to tell the game where the sound is coming from. + * + *

By default, {@link Source#MASTER} is used.

+ * + * @param source a source provider, evaluated eagerly + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder source(final Source.@NotNull Provider source); + + /** + * The volume for this sound, indicating how far away it can be heard. + * + *

Default value is {@code 1}.

+ * + * @param volume the sound volume + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder volume(final @Range(from = 0, to = Integer.MAX_VALUE) float volume); + + /** + * The volume for this sound, indicating how far away it can be heard. + * + *

Default value is {@code 1}.

+ * + * @param pitch the sound pitch + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder pitch(final @Range(from = -1, to = 1) float pitch); + + /** + * The seed for this sound, used for weighted choices. + * + *

The default seed is the world seed of the receiver's current world.

+ * + * @param seed the seed + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder seed(final long seed); + + /** + * The seed for this sound, used for weighted choices. + * + *

The default seed is the world seed of the receiver's current world.

+ * + * @param seed the seed + * @return this builder + * @since 4.12.0 + */ + @NotNull Builder seed(final @NotNull OptionalLong seed); + } } 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 2429ee716..de1625b9d 100644 --- a/api/src/main/java/net/kyori/adventure/sound/SoundImpl.java +++ b/api/src/main/java/net/kyori/adventure/sound/SoundImpl.java @@ -23,12 +23,18 @@ */ package net.kyori.adventure.sound; +import java.util.OptionalLong; +import java.util.function.Supplier; import java.util.stream.Stream; import net.kyori.adventure.internal.Internals; +import net.kyori.adventure.key.Key; import net.kyori.adventure.util.ShadyPines; import net.kyori.examination.ExaminableProperty; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; + +import static java.util.Objects.requireNonNull; abstract class SoundImpl implements Sound { static final Emitter EMITTER_SELF = new Emitter() { @@ -41,12 +47,14 @@ public String toString() { private final Source source; private final float volume; private final float pitch; + private final OptionalLong seed; private SoundStop stop; - SoundImpl(final @NotNull Source source, final float volume, final float pitch) { + SoundImpl(final @NotNull Source source, final float volume, final float pitch, final OptionalLong seed) { this.source = source; this.volume = volume; this.pitch = pitch; + this.seed = seed; } @Override @@ -64,6 +72,11 @@ public float pitch() { return this.pitch; } + @Override + public OptionalLong seed() { + return this.seed; + } + @Override public @NotNull SoundStop asStop() { if (this.stop == null) this.stop = SoundStop.namedOnSource(this.name(), this.source()); @@ -78,7 +91,8 @@ public boolean equals(final @Nullable Object other) { return this.name().equals(that.name()) && this.source == that.source && ShadyPines.equals(this.volume, that.volume) - && ShadyPines.equals(this.pitch, that.pitch); + && ShadyPines.equals(this.pitch, that.pitch) + && this.seed.equals(that.seed); } @Override @@ -87,6 +101,7 @@ public int hashCode() { result = (31 * result) + this.source.hashCode(); result = (31 * result) + Float.hashCode(this.volume); result = (31 * result) + Float.hashCode(this.pitch); + result = (31 * result) + this.seed.hashCode(); return result; } @@ -96,7 +111,8 @@ public int hashCode() { ExaminableProperty.of("name", this.name()), ExaminableProperty.of("source", this.source), ExaminableProperty.of("volume", this.volume), - ExaminableProperty.of("pitch", this.pitch) + ExaminableProperty.of("pitch", this.pitch), + ExaminableProperty.of("seed", this.seed) ); } @@ -104,4 +120,128 @@ public int hashCode() { public String toString() { return Internals.toString(this); } + + static final class BuilderImpl implements Builder { + private static final float DEFAULT_VOLUME = 1f; + private static final float DEFAULT_PITCH = 1f; + private Key eagerType; + private Supplier lazyType; + private Source source = Source.MASTER; + private float volume = DEFAULT_VOLUME; + private float pitch = DEFAULT_PITCH; + private OptionalLong seed = OptionalLong.empty(); + + BuilderImpl() { + } + + BuilderImpl(final @NotNull Sound existing) { + if (existing instanceof Eager) { + this.type(((Eager) existing).name); + } else if (existing instanceof Lazy) { + this.type(((Lazy) existing).supplier); + } else { + throw new IllegalArgumentException("Unknown sound type " + existing + ", must be Eager or Lazy"); + } + + this.source(existing.source()) + .volume(existing.volume()) + .pitch(existing.pitch()) + .seed(existing.seed()); + } + + @Override + public @NotNull Builder type(final @NotNull Key type) { + this.eagerType = requireNonNull(type, "type"); + this.lazyType = null; + return this; + } + + @Override + public @NotNull Builder type(final @NotNull Type type) { + this.eagerType = requireNonNull(requireNonNull(type, "type").key(), "type.key()"); + this.lazyType = null; + return this; + } + + @Override + public @NotNull Builder type(final @NotNull Supplier typeSupplier) { + this.lazyType = requireNonNull(typeSupplier, "typeSupplier"); + this.eagerType = null; + return this; + } + + @Override + public @NotNull Builder source(final @NotNull Source source) { + this.source = requireNonNull(source, "source"); + return this; + } + + @Override + public @NotNull Builder source(final Source.@NotNull Provider source) { + return this.source(source.soundSource()); + } + + @Override + public @NotNull Builder volume(final @Range(from = 0, to = Integer.MAX_VALUE) float volume) { + this.volume = volume; + return this; + } + + @Override + public @NotNull Builder pitch(final @Range(from = -1, to = 1) float pitch) { + this.pitch = pitch; + return this; + } + + @Override + public @NotNull Builder seed(final long seed) { + this.seed = OptionalLong.of(seed); + return this; + } + + @Override + public @NotNull Builder seed(final @NotNull OptionalLong seed) { + this.seed = requireNonNull(seed, "seed"); + return this; + } + + @Override + public @NotNull Sound build() { + if (this.eagerType != null) { + return new Eager(this.eagerType, this.source, this.volume, this.pitch, this.seed); + } else if (this.lazyType != null) { + return new Lazy(this.lazyType, this.source, this.volume, this.pitch, this.seed); + } else { + throw new IllegalStateException("A sound type must be provided to build a sound"); + } + } + } + + static final class Eager extends SoundImpl { + final Key name; + + Eager(final @NotNull Key name, final @NotNull Source source, final float volume, final float pitch, final OptionalLong seed) { + super(source, volume, pitch, seed); + this.name = name; + } + + @Override + public @NotNull Key name() { + return this.name; + } + } + + static final class Lazy extends SoundImpl { + final Supplier supplier; + + Lazy(final @NotNull Supplier supplier, final @NotNull Source source, final float volume, final float pitch, final OptionalLong seed) { + super(source, volume, pitch, seed); + this.supplier = supplier; + } + + @Override + public @NotNull Key name() { + return this.supplier.get().key(); + } + } } diff --git a/api/src/test/java/net/kyori/adventure/sound/SoundTest.java b/api/src/test/java/net/kyori/adventure/sound/SoundTest.java index ee7f20b10..a43520d3f 100644 --- a/api/src/test/java/net/kyori/adventure/sound/SoundTest.java +++ b/api/src/test/java/net/kyori/adventure/sound/SoundTest.java @@ -24,6 +24,7 @@ package net.kyori.adventure.sound; import com.google.common.testing.EqualsTester; +import java.util.OptionalLong; import net.kyori.adventure.key.Key; import org.junit.jupiter.api.Test; @@ -35,11 +36,12 @@ class SoundTest { @Test void testGetters() { - final Sound sound = Sound.sound(SOUND_KEY, Sound.Source.HOSTILE, 1f, 1f); + final Sound sound = Sound.sound(b -> b.type(SOUND_KEY).source(Sound.Source.HOSTILE).pitch(1f).volume(1f).seed(OptionalLong.of(666))); assertEquals(SOUND_KEY, sound.name()); assertEquals(Sound.Source.HOSTILE, sound.source()); assertEquals(1f, sound.volume()); assertEquals(1f, sound.pitch()); + assertEquals(666, sound.seed().getAsLong()); } @Test diff --git a/serializer-configurate3/src/main/java/net/kyori/adventure/serializer/configurate3/SoundSerializer.java b/serializer-configurate3/src/main/java/net/kyori/adventure/serializer/configurate3/SoundSerializer.java index 615b99165..09fe3a450 100644 --- a/serializer-configurate3/src/main/java/net/kyori/adventure/serializer/configurate3/SoundSerializer.java +++ b/serializer-configurate3/src/main/java/net/kyori/adventure/serializer/configurate3/SoundSerializer.java @@ -24,6 +24,7 @@ package net.kyori.adventure.serializer.configurate3; import com.google.common.reflect.TypeToken; +import java.util.OptionalLong; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import ninja.leaping.configurate.ConfigurationNode; @@ -42,6 +43,7 @@ final class SoundSerializer implements TypeSerializer { static final String SOURCE = "source"; static final String PITCH = "pitch"; static final String VOLUME = "volume"; + static final String SEED = "seed"; private SoundSerializer() { } @@ -52,16 +54,24 @@ private SoundSerializer() { return null; } + final Sound.Builder builder = Sound.sound(); final Key name = value.getNode(NAME).getValue(KeySerializer.INSTANCE.type()); final Sound.Source source = value.getNode(SOURCE).getValue(SOURCE_TYPE); - final float volume = value.getNode(VOLUME).getFloat(1.0f); - final float pitch = value.getNode(PITCH).getFloat(1.0f); - if (name == null || source == null) { throw new ObjectMappingException("A name and source are required to deserialize a Sound"); } - return Sound.sound(name, source, volume, pitch); + builder + .type(name) + .source(source) + .volume(value.getNode(VOLUME).getFloat(1.0f)) + .pitch(value.getNode(PITCH).getFloat(1.0f)); + final ConfigurationNode seed = value.getNode(SEED); + if (!seed.isVirtual()) { + builder.seed(OptionalLong.of(seed.getLong())); + } + + return builder.build(); } @Override @@ -74,5 +84,10 @@ public void serialize(final @NotNull TypeToken type, final @Nullable Sound ob value.getNode(SOURCE).setValue(SOURCE_TYPE, obj.source()); value.getNode(VOLUME).setValue(obj.volume()); value.getNode(PITCH).setValue(obj.pitch()); + if (obj.seed().isPresent()) { + value.getNode(SEED).setValue(obj.seed().getAsLong()); + } else { + value.getNode(SEED).setValue(null); + } } } diff --git a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SoundSerializer.java b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SoundSerializer.java index 2d0c08540..935f80df7 100644 --- a/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SoundSerializer.java +++ b/serializer-configurate4/src/main/java/net/kyori/adventure/serializer/configurate4/SoundSerializer.java @@ -24,6 +24,7 @@ package net.kyori.adventure.serializer.configurate4; import java.lang.reflect.Type; +import java.util.OptionalLong; import net.kyori.adventure.key.Key; import net.kyori.adventure.sound.Sound; import org.jetbrains.annotations.NotNull; @@ -39,6 +40,7 @@ final class SoundSerializer implements TypeSerializer { static final String SOURCE = "source"; static final String PITCH = "pitch"; static final String VOLUME = "volume"; + static final String SEED = "seed"; private SoundSerializer() { } @@ -49,16 +51,23 @@ private SoundSerializer() { return null; } + final Sound.Builder builder = Sound.sound(); final Key name = value.node(NAME).get(Key.class); final Sound.Source source = value.node(SOURCE).get(Sound.Source.class); - final float volume = value.node(VOLUME).getFloat(1.0f); - final float pitch = value.node(PITCH).getFloat(1.0f); - if (name == null || source == null) { throw new SerializationException("A name and source are required to deserialize a Sound"); } + builder + .type(name) + .source(source) + .volume(value.node(VOLUME).getFloat(1.0f)) + .pitch(value.node(PITCH).getFloat(1.0f)); + final ConfigurationNode seed = value.node(SEED); + if (!seed.virtual()) { + builder.seed(OptionalLong.of(seed.getLong())); + } - return Sound.sound(name, source, volume, pitch); + return builder.build(); } @Override @@ -71,5 +80,10 @@ public void serialize(final @NotNull Type type, final @Nullable Sound obj, final value.node(SOURCE).set(Sound.Source.class, obj.source()); value.node(VOLUME).set(obj.volume()); value.node(PITCH).set(obj.pitch()); + if (obj.seed().isPresent()) { + value.node(SEED).set(obj.seed().getAsLong()); + } else { + value.node(SEED).set(null); + } } }