diff --git a/build.gradle.kts b/build.gradle.kts index 8f513b8..8e6919a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,10 +3,10 @@ plugins { alias(libs.plugins.kotlinMultiplatform) apply false alias(libs.plugins.vanniktech.mavenPublish) apply false - kotlin("plugin.serialization") version "2.1.20" apply false + kotlin("plugin.serialization") version "2.3.0" apply false id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" apply false - id("org.jetbrains.dokka") version "2.0.0" + id("org.jetbrains.dokka") version "2.1.0" } subprojects { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 14d1b06..3805cc5 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -7,7 +7,7 @@ plugins { //alias(libs.plugins.androidLibrary) alias(libs.plugins.vanniktech.mavenPublish) - kotlin("plugin.serialization") version "2.1.20" + kotlin("plugin.serialization") version "2.3.0" id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.17.0" } @@ -83,7 +83,7 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1") + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") } } val commonTest by getting { diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/RowPattern.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/RowPattern.kt new file mode 100644 index 0000000..78ef44e --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/RowPattern.kt @@ -0,0 +1,133 @@ +package cn.rdlevel.rdkt.core.data + +import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI +import cn.rdlevel.rdkt.core.data.RowPattern.Companion.ALLOWED_CHARS +import cn.rdlevel.rdkt.core.serialization.TransformSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer + +/** + * Represents the pattern of pulses of a row. + */ +@Serializable(RowPattern.Serializer::class) +public class RowPattern { + /** + * The pattern string representing the pulse pattern of the row. It must be exactly 6 characters long, and each character must be one of the allowed characters defined in [ALLOWED_CHARS]. + */ + public var pattern: String = "------" + set(value) { + require(value.length == 6) { "Pulse pattern must be 6 characters long." } + require(value.all { it in ALLOWED_CHARS }) { + "Pulse pattern can only contain '$ALLOWED_CHARS', but incorrect pattern found: '$value'." + } + field = value + } + + /** + * Sets the pulse pattern using a list of [Pulse]s. The list must contain exactly 6 pulses. + */ + public fun setPattern( + pulses: List, + ) { + pattern = pulses.joinToString(separator = "") { it.char.toString() } + } + + /** + * Sets the pulse pattern using individual [Pulse] parameters. + */ + public fun setPattern( + pulse1: Pulse, + pulse2: Pulse, + pulse3: Pulse, + pulse4: Pulse, + pulse5: Pulse, + pulse6: Pulse, + ) { + setPattern(listOf(pulse1, pulse2, pulse3, pulse4, pulse5, pulse6)) + } + + /** + * Constructs a [RowPattern] with the specified pattern string. The pattern string must be exactly 6 characters long, and each character must be one of the allowed characters defined in [ALLOWED_CHARS]. + */ + public constructor( + pattern: String = "------", + ) { + this.pattern = pattern + } + + /** + * Constructs a [RowPattern] with the specified list of [Pulse]s. The list must contain exactly 6 pulses. + */ + public constructor( + pulses: List, + ) { + setPattern(pulses) + } + + /** + * Constructs a [RowPattern] with the specified individual [Pulse] parameters. + */ + public constructor( + pulse1: Pulse, + pulse2: Pulse, + pulse3: Pulse, + pulse4: Pulse, + pulse5: Pulse, + pulse6: Pulse, + ) { + setPattern(listOf(pulse1, pulse2, pulse3, pulse4, pulse5, pulse6)) + } + + /** + * Represents the pattern of a pulse. + */ + public enum class Pulse(public val char: Char) { + /** + * The pulse is normal. + */ + NORMAL('-'), + + /** + * The pulse is covered by x, indicating not having pulse sound. + */ + X('x'), + + /** + * The pulse is up-shaped. + */ + UP('u'), + + /** + * The pulse is down-shaped. + */ + DOWN('d'), + + /** + * The pulse is blocked, indicating a longer pulse duration. + */ + BLOCKED('b'), + + /** + * The pulse is resumed to normal, indicating the end of a blocked pulse. + */ + RESUME('r'), + } + + public companion object { + /** + * Characters allowed in the pattern string, corresponding to the defined pulse types. + */ + public const val ALLOWED_CHARS: String = "-xudbr" + } + + @OptIn(RDKTInternalAPI::class) + public object Serializer : TransformSerializer(String.serializer()) { + override fun toData(value: RowPattern): String { + return value.pattern + } + + override fun fromData(data: String): RowPattern { + return RowPattern(data) + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/SelectedRoomsAndTopLayer.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/SelectedRoomsAndTopLayer.kt index 48151c4..6d6a959 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/SelectedRoomsAndTopLayer.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/SelectedRoomsAndTopLayer.kt @@ -1,10 +1,10 @@ @file:OptIn(RDKTInternalAPI::class) -@file:JvmName("SelectedRoomsUtils") +@file:JvmName("SelectedRoomUtil") package cn.rdlevel.rdkt.core.data import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI -import cn.rdlevel.rdkt.core.serializers.TransformSerializer +import cn.rdlevel.rdkt.core.serialization.TransformSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.SetSerializer import kotlinx.serialization.builtins.serializer @@ -79,7 +79,7 @@ public sealed interface SelectedRoomsAndTopLayer { } public object Serializer : - TransformSerializer>(SetSerializer(Int.serializer())) { + TransformSerializer>(kotlinx.serialization.serializer()) { override fun toData(value: SelectedRoomsAndTopLayer): Set { return value.rooms } @@ -403,6 +403,7 @@ public fun singleRoomOf(kClass: KClass, ro * @param rooms The set of room IDs to include in the selection. * @return An instance of [SelectedRoomsAndTopLayer] of the specified type. */ +@JvmName("of") public inline fun roomsOf(rooms: Set): T { return roomsOf(T::class, rooms) } @@ -415,6 +416,7 @@ public inline fun roomsOf(rooms: Set * @param rooms Additional room IDs to include in the selection. * @return An instance of [SelectedRoomsAndTopLayer] of the specified type. */ +@JvmName("of") public inline fun roomsOf(room: Int, vararg rooms: Int): T { return roomsOf(T::class, room, *rooms) } @@ -426,6 +428,7 @@ public inline fun roomsOf(room: Int, vara * @param room The ID of the selected room. * @return An instance of [SingleSelectedRoomOrTopLayer] of the specified type. */ +@JvmName("ofSingle") public inline fun singleRoomOf(room: Int): T { return singleRoomOf(T::class, room) } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/AudioData.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/AudioData.kt new file mode 100644 index 0000000..4756970 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/AudioData.kt @@ -0,0 +1,229 @@ +@file:JvmName("AudioDataUtil") + +package cn.rdlevel.rdkt.core.data.sound + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads + +/** + * Represents an abstraction of audio and its configuration. + */ +public sealed class AbstractAudioData { + /** + * The file name or built-in name of the audio. + */ + @SerialName("filename") + public abstract var fileName: String + + /** + * The volume of the audio, from 0 to 300. + */ + public var volume: Int = 100 + set(value) { + require(value in 0..300) { "Volume must be between 0 and 300." } + field = value + } + + /** + * The pitch of the audio, from 0 to 300. + */ + public var pitch: Int = 100 + set(value) { + require(value in 0..300) { "Pitch must be between 0 and 300." } + field = value + } + + /** + * The pan of the audio, from -100 to 100. + * Negative values pan to the left, positive values pan to the right. + */ + public var pan: Int = 0 + set(value) { + require(value in -100..100) { "Pan must be between -100 and 100." } + field = value + } + + /** + * The offset in milliseconds to start the audio from. + * When the [fileName] is built-in, this property is ignored. + */ + public var offset: Int = 0 + + override fun toString(): String { + return "AbstractAudioData(fileName='$fileName', volume=$volume, pitch=$pitch, pan=$pan, offset=$offset)" + } +} + +/** + * Represents an audio and its configuration. + */ +@Serializable +public data class AudioData( + @SerialName("filename") + override var fileName: String, +) : AbstractAudioData() + + +/** + * Creates an [AudioData] instance with the specified [fileName], [volume], [pitch], [pan], and [offset]. + * + * @param fileName The file name or built-in name of the audio. + * @param volume The volume of the audio, from 0 to 300. Default is 100. + * @param pitch The pitch of the audio, from 0 to 300. Default is 100. + * @param pan The pan of the audio, from -100 to 100. Default is 0. + * @param offset The offset in milliseconds to start the audio from. Default is 0. + * + * @return An [AudioData] instance with the specified properties. + */ +@JvmOverloads +@JvmName("ofAudio") +public fun audioDataOf( + fileName: String, + volume: Int = 100, + pitch: Int = 100, + pan: Int = 0, + offset: Int = 0, +): AudioData { + return AudioData(fileName).apply { + this.volume = volume + this.pitch = pitch + this.pan = pan + this.offset = offset + } +} + +/** + * Converts an [AbstractAudioData] to an [AudioData]. + * + * @param audioData The audio data to convert. + * @return An [AudioData] instance with the same properties as the given [audioData]. + */ +@JvmName("ofAudio") +public fun audioDataOf(audioData: AbstractAudioData): AudioData { + return AudioData(audioData.fileName).apply { + this.volume = audioData.volume + this.pitch = audioData.pitch + this.pan = audioData.pan + } +} + + +/** + * Creates a [AudioData] instance with the specified [fileName], [offset], [volume], [pitch], and [pan]. + * This is a convenient function to create a [AudioData] for music quickly. + * + * @param fileName The file name or built-in name of the audio. + * @param offset The offset in milliseconds to start the audio from. Default is 0. + * @param volume The volume of the audio, from 0 to 300. Default is 100. + * @param pitch The pitch of the audio, from 0 to 300. Default is 100. + * @param pan The pan of the audio, from -100 to 100. Default is 0. + * + * @return A [AudioData] instance with the specified properties. + */ +@JvmOverloads +@JvmName("ofMusic") +public fun audioDataOfMusic( + fileName: String, + offset: Int = 0, + volume: Int = 100, + pitch: Int = 100, + pan: Int = 0 +): AudioData { + return AudioData(fileName).apply { + this.volume = volume + this.pitch = pitch + this.pan = pan + this.offset = offset + } +} + +/** + * Represents a subgroup audio and its configuration. + * + * This is used for [SetGameSoundEvent][cn.rdlevel.rdkt.core.events.SetGameSoundEvent]. + */ +@Serializable +public open class SubGroupAudioData @JvmOverloads constructor( + /** + * The file name or built-in name of the audio. + * Leave blank to keep the audio unchanged. + */ + @SerialName("filename") + override var fileName: String = "", + /** + * Indicates whether this subgroup audio setting is enabled in the editor. + */ + public var used: Boolean = true +) : AbstractAudioData() { + override fun toString(): String { + return "SubGroupAudioData(fileName='$fileName', used=$used, volume=$volume, pitch=$pitch, pan=$pan, offset=$offset)" + } +} + +/** + * Creates a [SubGroupAudioData] instance with the specified [fileName], [used], [volume], [pitch], and [pan]. + * + * @param fileName The file name or built-in name of the audio. Default is an empty string. + * @param used Indicates whether this subgroup audio setting is enabled in the editor. Default is true. + * @param volume The volume of the audio, from 0 to 300. Default is 100. + * @param pitch The pitch of the audio, from 0 to 300. Default is 100. + * @param pan The pan of the audio, from -100 to 100. Default is 0. + * + * @return A [SubGroupAudioData] instance with the specified properties. + */ +@JvmOverloads +@JvmName("ofSubGroup") +public fun subGroupAudioDataOf( + fileName: String = "", + used: Boolean = true, + volume: Int = 100, + pitch: Int = 100, + pan: Int = 0 +): SubGroupAudioData { + return SubGroupAudioData(fileName, used).apply { + this.volume = volume + this.pitch = pitch + this.pan = pan + } +} + +/** + * Converts an [AbstractAudioData] to a [SubGroupAudioData]. + * + * @param audioData The audio data to convert. + * @return A [SubGroupAudioData] instance with the same properties as the given [audioData]. + */ +@JvmName("ofSubGroup") +public fun subGroupAudioDataOf(audioData: AbstractAudioData): SubGroupAudioData { + return SubGroupAudioData(audioData.fileName).apply { + this.volume = audioData.volume + this.pitch = audioData.pitch + this.pan = audioData.pan + + if (audioData is SubGroupAudioData) { + this.used = audioData.used + } + } +} + +/** + * Copies a [SubGroupAudioData]. + * + * @param audioData The audio data to copy. + * @return A [SubGroupAudioData] instance with the same properties as the given [audioData]. + */ +@JvmName("ofSubGroup") +public fun subGroupAudioDataOf(audioData: SubGroupAudioData): SubGroupAudioData { + return SubGroupAudioData(audioData.fileName, audioData.used).apply { + this.volume = audioData.volume + this.pitch = audioData.pitch + this.pan = audioData.pan + } +} + +/** + * A predefined instance of [SubGroupAudioData] that is marked as unused. + */ +public val unusedSubGroupAudioData: SubGroupAudioData = subGroupAudioDataOf(used = false) \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/VoiceSource.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/VoiceSource.kt new file mode 100644 index 0000000..20715de --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/VoiceSource.kt @@ -0,0 +1,205 @@ +@file:OptIn(RDKTInternalAPI::class) + +package cn.rdlevel.rdkt.core.data.sound + +import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI +import kotlin.jvm.JvmStatic + +/** + * A voice source that provides counting sounds. + * Used in [cn.rdlevel.rdkt.core.events.SetCountingSoundEvent] to specify the source of counting sounds. + */ +public sealed interface VoiceSource { + public val type: String + + /** + * A classic voice source that counts classic rows. + */ + public sealed interface Classic : VoiceSource { + @RDKTInternalAPI + public sealed class AbstractClassic(override val type: String) : Classic + + public object JyiCount : AbstractClassic("JyiCount") + + public object JyiCountFast : AbstractClassic("JyiCountFast") + + public object JyiCountCalm : AbstractClassic("JyiCountCalm") + + public object JyiCountTired : AbstractClassic("JyiCountTired") + + public object JyiCountVeryTired : AbstractClassic("JyiCountVeryTired") + + public object JyiCountJapanese : AbstractClassic("JyiCountJapanese") + + public object JyiCountLegacy : AbstractClassic("JyiCountLegacy") + + public object IanCount : AbstractClassic("IanCount") + + public object IanCountFast : AbstractClassic("IanCountFast") + + public object IanCountCalm : AbstractClassic("IanCountCalm") + + public object IanCountSlow : AbstractClassic("IanCountSlow") + + public object IanCountSlower : AbstractClassic("IanCountSlower") + + public object WhistleCount : AbstractClassic("WhistleCount") + + public object BirdCount : AbstractClassic("BirdCount") + + public object ParrotCount : AbstractClassic("ParrotCount") + + public object OwlCount : AbstractClassic("OwlCount") + + public object OrioleCount : AbstractClassic("OrioleCount") + + public object WrenCount : AbstractClassic("WrenCount") + + public object CanaryCount : AbstractClassic("CanaryCount") + + public object SpearCount : AbstractClassic("SpearCount") + } + + /** + * A oneshot voice source that counts oneshot rows. + */ + public sealed interface Oneshot : VoiceSource { + @RDKTInternalAPI + public sealed class AbstractOneshot(override val type: String) : Oneshot + + /** + * Nurse oneshot voice source. + */ + public object JyiCountEnglish : AbstractOneshot("JyiCountEnglish") + + /** + * Ian oneshot voice source. + */ + public object IanCountEnglish : AbstractOneshot("IanCountEnglish") + + /** + * Ian calm oneshot voice source. + */ + public object IanCountEnglishCalm : AbstractOneshot("IanCountEnglishCalm") + + /** + * Ian slow oneshot voice source. + */ + public object IanCountEnglishSlow : AbstractOneshot("IanCountEnglishSlow") + } + + /** + * A custom voice source that allows custom sounds. + * + * @property sounds The array of custom sounds. For classic rows, it should have 7 sounds corresponding to the 7 pulses. For oneshot rows, it should have 10 sounds corresponding to the 10 subdivisions. + */ + public class Custom internal constructor( + public val sounds: Array, + ) : Classic, Oneshot { + override val type: String = TYPE + + public companion object { + /** + * The type of custom voice sources. + */ + public const val TYPE: String = "Custom" + + /** + * Creates a custom voice source with the given sounds for classic rows. + * The sounds array must have exactly 7 sounds, corresponding to the 7 pulses. + */ + @JvmStatic + public fun ofClassic(sounds: Array): Classic { + require(sounds.size == 7) { "Classic voice source must have exactly 7 sounds." } + return Custom(sounds) + } + + /** + * Creates a custom voice source with the given sounds for classic rows. + * The sounds must correspond to the 7 pulses in order. + */ + public fun ofClassic( + pulse1: AudioData, + pulse2: AudioData, + pulse3: AudioData, + pulse4: AudioData, + pulse5: AudioData, + pulse6: AudioData, + pulse7: AudioData, + ): Classic = ofClassic(arrayOf(pulse1, pulse2, pulse3, pulse4, pulse5, pulse6, pulse7)) + + /** + * Creates a custom voice source with the given sounds for oneshot rows. + * The sounds array must have exactly 10 sounds, corresponding to the 10 subdivisions. + */ + @JvmStatic + public fun ofOneshot(sounds: Array): Oneshot { + require(sounds.size == 10) { "Oneshot voice source must have exactly 10 sounds." } + return Custom(sounds) + } + + /** + * Creates a custom voice source with the given sounds for oneshot rows. + * The sounds must correspond to the 10 subdivisions in order. + */ + public fun ofOneshot( + subdivision1: AudioData, + subdivision2: AudioData, + subdivision3: AudioData, + subdivision4: AudioData, + subdivision5: AudioData, + subdivision6: AudioData, + subdivision7: AudioData, + subdivision8: AudioData, + subdivision9: AudioData, + subdivision10: AudioData, + ): Oneshot = ofOneshot( + arrayOf( + subdivision1, + subdivision2, + subdivision3, + subdivision4, + subdivision5, + subdivision6, + subdivision7, + subdivision8, + subdivision9, + subdivision10 + ) + ) + } + } + + public companion object { + private val typeToVoiceSourceMap: Map = listOf( + Classic.JyiCount, + Classic.JyiCountFast, + Classic.JyiCountCalm, + Classic.JyiCountTired, + Classic.JyiCountVeryTired, + Classic.JyiCountJapanese, + Classic.JyiCountLegacy, + Classic.IanCount, + Classic.IanCountFast, + Classic.IanCountCalm, + Classic.IanCountSlow, + Classic.IanCountSlower, + Classic.WhistleCount, + Classic.BirdCount, + Classic.ParrotCount, + Classic.OwlCount, + Classic.OrioleCount, + Classic.WrenCount, + Classic.CanaryCount, + Classic.SpearCount, + Oneshot.JyiCountEnglish, + Oneshot.IanCountEnglish, + Oneshot.IanCountEnglishCalm, + Oneshot.IanCountEnglishSlow + ).associateBy { it.type } + + internal fun fromType(type: String): VoiceSource? { + return typeToVoiceSourceMap[type] + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/AudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/AudioGroup.kt new file mode 100644 index 0000000..c5d9bf0 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/AudioGroup.kt @@ -0,0 +1,171 @@ +@file:OptIn(RDKTInternalAPI::class) +@file:JvmName("AudioGroups") + +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.serialization.PolymorphicDelegatedSerializer +import cn.rdlevel.rdkt.core.serialization.TransformSerializer +import cn.rdlevel.rdkt.core.util.smallCamelToBigCamel +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlin.jvm.JvmName +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic +import kotlin.jvm.JvmSynthetic +import kotlin.reflect.KProperty + +/** + * Represents a group of audio subtypes and their configurations. + * This is used in [SetGameSoundEvent][cn.rdlevel.rdkt.core.events.SetGameSoundEvent]. + * + * @property subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(AudioGroup.Serializer::class) +public abstract class AudioGroup( + protected val subTypes: MutableMap = mutableMapOf(), +) : Map by subTypes { + /** + * The type of sound group. + */ + public abstract val soundType: String + + protected operator fun getValue(thisRef: AudioGroup, property: KProperty<*>): SubGroupAudioData { + return subTypes[property.name.smallCamelToBigCamel()] ?: error("Subtype '${property.name}' not found!") + } + + protected operator fun setValue(thisRef: AudioGroup, property: KProperty<*>, value: SubGroupAudioData) { + subTypes[property.name.smallCamelToBigCamel()] = value + } + + override fun toString(): String { + return "AudioGroup(soundType='$soundType', subTypes=$subTypes)" + } + + public companion object { + private val conversionMap: MutableMap) -> AudioGroup> = mutableMapOf() + + /** + * Tries to convert the [AudioGroup] to the appropriate subclass based on the provided [type]. + * + * @param group the [AudioGroup] instance to convert. + * @param type The type identifier used to determine the target subclass. + * @return The converted [AudioGroup] if a matching converter is found; otherwise, returns the instance itself. + */ + @JvmStatic + public fun convertGroupTo(group: AudioGroup, type: String): AudioGroup { + val converter = conversionMap[type] + ?: return group + return converter(group) + } + + internal fun registerConversion( + type: String, + converter: (Map) -> AudioGroup + ) { + require(type !in conversionMap) { + "Conversion for type '$type' is already registered." + } + + conversionMap[type] = converter + } + + init { + Serializer.register() + } + } + + /** + * A common serializer for [AudioGroup] subclasses. + * Subclass implementations should be singleton and call [register] to register their conversion function before use. + * + * @param T The type of [AudioGroup]. + * @param soundType The type of sound group. + */ + public abstract class BaseSerializer(protected val soundType: String) : + TransformSerializer>(kotlinx.serialization.serializer()) { + /** + * Creates an instance of [T] from a map of subtype identifiers to [SubGroupAudioData]. + */ + protected abstract fun toAudioGroup(map: Map): T + + /** + * Registers the conversion function for this [AudioGroup] subclass. + */ + public fun register() { + registerConversion(soundType, ::toAudioGroup) + } + + override fun toData(value: T): List { + return value.map { (subType, data) -> + data.toRaw(subType) + } + } + + override fun fromData(data: List): T { + val map = data.associateBy { it.groupSubtype } + return toAudioGroup(map) + } + } + + public object Serializer : BaseSerializer("Custom") { + override fun toAudioGroup(map: Map): AudioGroup { + return CustomAudioGroup(map.toMutableMap()) + } + } +} + +/** + * Tries to convert the [AudioGroup] to the appropriate subclass based on the provided [type]. + * + * @param type The type identifier used to determine the target subclass. + * @return The converted [AudioGroup] if a matching converter is found; otherwise, returns the instance itself. + */ +@JvmSynthetic +public fun AudioGroup.tryConvertTo(type: String): AudioGroup { + return AudioGroup.convertGroupTo(this, type) +} + +/** + * Represents the data form of audio data for a specific subgroup within an [AudioGroup]. + * + * @property groupSubtype The identifier for the subgroup type. + */ +@Serializable +@RDKTInternalAPI +public data class RawSubGroupAudioData( + public val groupSubtype: String, +) : SubGroupAudioData() { + override fun toString(): String { + return super.toString() + } +} + +private fun SubGroupAudioData.toRaw(groupSubtype: String): RawSubGroupAudioData { + val data = this + return RawSubGroupAudioData(groupSubtype).apply { + fileName = data.fileName + used = data.used + volume = data.volume + pitch = data.pitch + pan = data.pan + } +} + +/** + * A customizable audio group that can hold any number of subtypes. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + * @param soundType The type of sound group. Default is "Custom". + */ +@Suppress("DELEGATED_MEMBER_HIDES_SUPERTYPE_OVERRIDE") +@Serializable(CustomAudioGroup.Serializer::class) +public class CustomAudioGroup @JvmOverloads constructor( + subTypes: MutableMap = mutableMapOf(), + @Transient + override var soundType: String = "Custom", +) : AudioGroup(subTypes), MutableMap by subTypes { + public object Serializer : + PolymorphicDelegatedSerializer(kotlinx.serialization.serializer()) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/BigMistakeAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/BigMistakeAudioGroup.kt new file mode 100644 index 0000000..30b20d5 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/BigMistakeAudioGroup.kt @@ -0,0 +1,47 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing big mistake sound. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(BigMistakeAudioGroup.Serializer::class) +public class BigMistakeAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "BigMistake" + + /** + * The audio data of the sound. + */ + public var bigMistake: SubGroupAudioData by this + + init { + bigMistake // check existence + } + + /** + * Creates a [BigMistakeAudioGroup] with the specified [bigMistake] audio data. + * + * @param bigMistake The audio data for the big mistake subtype. + */ + @JvmOverloads + public constructor(bigMistake: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("BigMistake" to bigMistake) + ) + + public companion object Serializer : BaseSerializer("BigMistake") { + override fun toAudioGroup(map: Map): BigMistakeAudioGroup { + return BigMistakeAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/BurnshotSoundAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/BurnshotSoundAudioGroup.kt new file mode 100644 index 0000000..6fae257 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/BurnshotSoundAudioGroup.kt @@ -0,0 +1,71 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import kotlinx.serialization.Serializable + +/** + * An audio group representing the sounds associated with a burnshot. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(BurnshotSoundAudioGroup.Serializer::class) +public class BurnshotSoundAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "BurnshotSound" + + /** + * The audio data of the first burnshot sound cue. + */ + public var burnshotSoundCueLow: SubGroupAudioData by this + + /** + * The audio data of the second burnshot sound cue. + */ + public var burnshotSoundCueHigh: SubGroupAudioData by this + + /** + * The audio data of the incoming burnshot sound. + */ + public var burnshotSoundRiser: SubGroupAudioData by this + + /** + * The audio data of the burnshot sound cymbal when hitting a burnshot. + */ + public var burnshotSoundCymbal: SubGroupAudioData by this + + init { + // check existence + burnshotSoundCueLow + burnshotSoundCueHigh + burnshotSoundRiser + burnshotSoundCymbal + } + + /** + * Creates a [BurnshotSoundAudioGroup] with the specified audio data. + */ + public constructor( + burnshotSoundCueLow: SubGroupAudioData, + burnshotSoundCueHigh: SubGroupAudioData, + burnshotSoundRiser: SubGroupAudioData, + burnshotSoundCymbal: SubGroupAudioData, + ) : this( + mapOf( + "BurnshotSoundCueLow" to burnshotSoundCueLow, + "BurnshotSoundCueHigh" to burnshotSoundCueHigh, + "BurnshotSoundRiser" to burnshotSoundRiser, + "BurnshotSoundCymbal" to burnshotSoundCymbal, + ), + ) + + public companion object Serializer : BaseSerializer("BurnshotSound") { + override fun toAudioGroup(map: Map): BurnshotSoundAudioGroup { + return BurnshotSoundAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/ClapSoundHoldAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/ClapSoundHoldAudioGroup.kt new file mode 100644 index 0000000..0a47c62 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/ClapSoundHoldAudioGroup.kt @@ -0,0 +1,72 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable + +/** + * An audio group representing the sound when the player is hitting with a hold. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(ClapSoundHoldAudioGroup.Serializer::class) +public class ClapSoundHoldAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "ClapSoundHold" + + /** + * The audio data of a short hold. + */ + public var clapSoundHoldShortStart: SubGroupAudioData by this + + /** + * The audio data of the end of a short hold. + */ + public var clapSoundHoldShortEnd: SubGroupAudioData by this + + /** + * The audio data of a long hold. + */ + public var clapSoundHoldLongStart: SubGroupAudioData by this + + /** + * The audio data of the end of a long hold. + */ + public var clapSoundHoldLongEnd: SubGroupAudioData by this + + init { + // check existence + clapSoundHoldShortStart + clapSoundHoldShortEnd + clapSoundHoldLongStart + clapSoundHoldLongEnd + } + + /** + * Creates a [ClapSoundHoldAudioGroup] with the specified audio data. + */ + public constructor( + clapSoundHoldShortStart: SubGroupAudioData = subGroupAudioDataOf(), + clapSoundHoldShortEnd: SubGroupAudioData = subGroupAudioDataOf(), + clapSoundHoldLongStart: SubGroupAudioData = subGroupAudioDataOf(), + clapSoundHoldLongEnd: SubGroupAudioData = subGroupAudioDataOf(), + ) : this( + mapOf( + "ClapSoundHoldShortStart" to clapSoundHoldShortStart, + "ClapSoundHoldShortEnd" to clapSoundHoldShortEnd, + "ClapSoundHoldLongStart" to clapSoundHoldLongStart, + "ClapSoundHoldLongEnd" to clapSoundHoldLongEnd, + ) + ) + + public companion object Serializer : BaseSerializer("ClapSoundHold") { + override fun toAudioGroup(map: Map): ClapSoundHoldAudioGroup { + return ClapSoundHoldAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/ClapSoundHoldP2AudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/ClapSoundHoldP2AudioGroup.kt new file mode 100644 index 0000000..5c27b66 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/ClapSoundHoldP2AudioGroup.kt @@ -0,0 +1,72 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable + +/** + * An audio group representing the sound when the player 2 is hitting with a hold. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(ClapSoundHoldP2AudioGroup.Serializer::class) +public class ClapSoundHoldP2AudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "ClapSoundHoldP2" + + /** + * The audio data of a short hold. + */ + public var clapSoundHoldShortStartP2: SubGroupAudioData by this + + /** + * The audio data of the end of a short hold. + */ + public var clapSoundHoldShortEndP2: SubGroupAudioData by this + + /** + * The audio data of a long hold. + */ + public var clapSoundHoldLongStartP2: SubGroupAudioData by this + + /** + * The audio data of the end of a long hold. + */ + public var clapSoundHoldLongEndP2: SubGroupAudioData by this + + init { + // check existence + clapSoundHoldShortStartP2 + clapSoundHoldShortEndP2 + clapSoundHoldLongStartP2 + clapSoundHoldLongEndP2 + } + + /** + * Creates a [ClapSoundHoldP2AudioGroup] with the specified audio data. + */ + public constructor( + clapSoundHoldShortStartP2: SubGroupAudioData = subGroupAudioDataOf(), + clapSoundHoldShortEndP2: SubGroupAudioData = subGroupAudioDataOf(), + clapSoundHoldLongStartP2: SubGroupAudioData = subGroupAudioDataOf(), + clapSoundHoldLongEndP2: SubGroupAudioData = subGroupAudioDataOf(), + ) : this( + mapOf( + "ClapSoundHoldShortStartP2" to clapSoundHoldShortStartP2, + "ClapSoundHoldShortEndP2" to clapSoundHoldShortEndP2, + "ClapSoundHoldLongStartP2" to clapSoundHoldLongStartP2, + "ClapSoundHoldLongEndP2" to clapSoundHoldLongEndP2, + ) + ) + + public companion object Serializer : BaseSerializer("ClapSoundHoldP2") { + override fun toAudioGroup(map: Map): ClapSoundHoldP2AudioGroup { + return ClapSoundHoldP2AudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/FreezeshotSoundAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/FreezeshotSoundAudioGroup.kt new file mode 100644 index 0000000..c27ee22 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/FreezeshotSoundAudioGroup.kt @@ -0,0 +1,71 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import kotlinx.serialization.Serializable + +/** + * An audio group representing the sounds associated with a freezeshot. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(FreezeshotSoundAudioGroup.Serializer::class) +public class FreezeshotSoundAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "FreezeshotSound" + + /** + * The audio data of the first freezeshot sound cue. + */ + public var freezeshotSoundCueLow: SubGroupAudioData by this + + /** + * The audio data of the second freezeshot sound cue. + */ + public var freezeshotSoundCueHigh: SubGroupAudioData by this + + /** + * The audio data of the incoming freezeshot sound. + */ + public var freezeshotSoundRiser: SubGroupAudioData by this + + /** + * The audio data of the freezeshot sound cymbal when hitting a freezeshot. + */ + public var freezeshotSoundCymbal: SubGroupAudioData by this + + init { + // check existence + freezeshotSoundCueLow + freezeshotSoundCueHigh + freezeshotSoundRiser + freezeshotSoundCymbal + } + + /** + * Creates a [FreezeshotSoundAudioGroup] with the specified audio data. + */ + public constructor( + freezeshotSoundCueLow: SubGroupAudioData, + freezeshotSoundCueHigh: SubGroupAudioData, + freezeshotSoundRiser: SubGroupAudioData, + freezeshotSoundCymbal: SubGroupAudioData, + ) : this( + mapOf( + "FreezeshotSoundCueLow" to freezeshotSoundCueLow, + "FreezeshotSoundCueHigh" to freezeshotSoundCueHigh, + "FreezeshotSoundRiser" to freezeshotSoundRiser, + "FreezeshotSoundCymbal" to freezeshotSoundCymbal, + ), + ) + + public companion object Serializer : BaseSerializer("FreezeshotSound") { + override fun toAudioGroup(map: Map): FreezeshotSoundAudioGroup { + return FreezeshotSoundAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/Hand1PopSoundAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/Hand1PopSoundAudioGroup.kt new file mode 100644 index 0000000..d4160dd --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/Hand1PopSoundAudioGroup.kt @@ -0,0 +1,47 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing the sound when the hand 1 presses the button on empty hit. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(Hand1PopSoundAudioGroup.Serializer::class) +public class Hand1PopSoundAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "Hand1PopSound" + + /** + * The audio data of the sound. + */ + public var hand1PopSound: SubGroupAudioData by this + + init { + hand1PopSound // check existence + } + + /** + * Creates a [Hand1PopSoundAudioGroup] with the specified [hand1PopSound] audio data. + * + * @param hand1PopSound The audio data for the Hand1PopSound subtype. + */ + @JvmOverloads + public constructor(hand1PopSound: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("Hand1PopSound" to hand1PopSound) + ) + + public companion object Serializer : BaseSerializer("Hand1PopSound") { + override fun toAudioGroup(map: Map): Hand1PopSoundAudioGroup { + return Hand1PopSoundAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/Hand2PopSoundAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/Hand2PopSoundAudioGroup.kt new file mode 100644 index 0000000..fa2111f --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/Hand2PopSoundAudioGroup.kt @@ -0,0 +1,47 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing the sound when the hand 2 presses the button on empty hit. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(Hand2PopSoundAudioGroup.Serializer::class) +public class Hand2PopSoundAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "Hand2PopSound" + + /** + * The audio data of the sound. + */ + public var hand2PopSound: SubGroupAudioData by this + + init { + hand2PopSound // check existence + } + + /** + * Creates a [Hand2PopSoundAudioGroup] with the specified [hand2PopSound] audio data. + * + * @param hand2PopSound The audio data for the Hand2PopSound subtype. + */ + @JvmOverloads + public constructor(hand2PopSound: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("Hand2PopSound" to hand2PopSound) + ) + + public companion object Serializer : BaseSerializer("Hand2PopSound") { + override fun toAudioGroup(map: Map): Hand2PopSoundAudioGroup { + return Hand2PopSoundAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosion2AudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosion2AudioGroup.kt new file mode 100644 index 0000000..0616705 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosion2AudioGroup.kt @@ -0,0 +1,45 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing the heart explosion sound after hitting 2 hits in a row. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(HeartExplosion2AudioGroup.Serializer::class) +public class HeartExplosion2AudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "HeartExplosion2" + + /** + * The audio data of the sound. + */ + public var heartExplosion2: SubGroupAudioData by this + + init { + heartExplosion2 // check existence + } + + /** + * Creates a [HeartExplosion2AudioGroup] with the specified [heartExplosion2] audio data. + */ + @JvmOverloads + public constructor(heartExplosion2: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("HeartExplosion2" to heartExplosion2) + ) + + public companion object Serializer : BaseSerializer("HeartExplosion2") { + override fun toAudioGroup(map: Map): HeartExplosion2AudioGroup { + return HeartExplosion2AudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosion3AudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosion3AudioGroup.kt new file mode 100644 index 0000000..0304aa8 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosion3AudioGroup.kt @@ -0,0 +1,45 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing the heart explosion sound after hitting 3 or more hits in a row. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(HeartExplosion3AudioGroup.Serializer::class) +public class HeartExplosion3AudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "HeartExplosion3" + + /** + * The audio data of the sound. + */ + public var heartExplosion3: SubGroupAudioData by this + + init { + heartExplosion3 // check existence + } + + /** + * Creates a [HeartExplosion3AudioGroup] with the specified [heartExplosion3] audio data. + */ + @JvmOverloads + public constructor(heartExplosion3: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("HeartExplosion3" to heartExplosion3) + ) + + public companion object Serializer : BaseSerializer("HeartExplosion3") { + override fun toAudioGroup(map: Map): HeartExplosion3AudioGroup { + return HeartExplosion3AudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosionAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosionAudioGroup.kt new file mode 100644 index 0000000..c4085b6 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/HeartExplosionAudioGroup.kt @@ -0,0 +1,47 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing the heart explosion sound after hitting single hit. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(HeartExplosionAudioGroup.Serializer::class) +public class HeartExplosionAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "HeartExplosion" + + /** + * The audio data of the sound. + */ + public var heartExplosion: SubGroupAudioData by this + + init { + heartExplosion // check existence + } + + /** + * Creates a [HeartExplosionAudioGroup] with the specified [heartExplosion] audio data. + * + * @param heartExplosion The audio data for the HeartExplosion subtype. + */ + @JvmOverloads + public constructor(heartExplosion: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("HeartExplosion" to heartExplosion) + ) + + public companion object Serializer : BaseSerializer("HeartExplosion") { + override fun toAudioGroup(map: Map): HeartExplosionAudioGroup { + return HeartExplosionAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/PulseSoundHoldAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/PulseSoundHoldAudioGroup.kt new file mode 100644 index 0000000..334c141 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/PulseSoundHoldAudioGroup.kt @@ -0,0 +1,88 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable + +/** + * An audio group representing the sound when a hold is pulsing. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(PulseSoundHoldAudioGroup.Serializer::class) +public class PulseSoundHoldAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "PulseSoundHold" + + /** + * The audio data of a hold pulse. + */ + public var pulseSoundHoldStart: SubGroupAudioData by this + + /** + * The audio data of the end of a short hold pulse. + */ + public var pulseSoundHoldShortEnd: SubGroupAudioData by this + + /** + * The audio data of the end of a hold pulse. + */ + public var pulseSoundHoldEnd: SubGroupAudioData by this + + /** + * The alternative audio data of a hold pulse. + */ + public var pulseSoundHoldStartAlt: SubGroupAudioData by this + + /** + * The alternative audio data of the end of a short hold pulse. + */ + public var pulseSoundHoldShortEndAlt: SubGroupAudioData by this + + /** + * The alternative audio data of the end of a hold pulse. + */ + public var pulseSoundHoldEndAlt: SubGroupAudioData by this + + init { + // check existence + pulseSoundHoldStart + pulseSoundHoldShortEnd + pulseSoundHoldEnd + pulseSoundHoldStartAlt + pulseSoundHoldShortEndAlt + pulseSoundHoldEndAlt + } + + /** + * Creates a [ClapSoundHoldAudioGroup] with the specified audio data. + */ + public constructor( + pulseSoundHoldStart: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldShortEnd: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldEnd: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldStartAlt: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldShortEndAlt: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldEndAlt: SubGroupAudioData = subGroupAudioDataOf(), + ) : this( + mapOf( + "PulseSoundHoldStart" to pulseSoundHoldStart, + "PulseSoundHoldShortEnd" to pulseSoundHoldShortEnd, + "PulseSoundHoldEnd" to pulseSoundHoldEnd, + "PulseSoundHoldStartAlt" to pulseSoundHoldStartAlt, + "PulseSoundHoldShortEndAlt" to pulseSoundHoldShortEndAlt, + "PulseSoundHoldEndAlt" to pulseSoundHoldEndAlt, + ) + ) + + public companion object Serializer : BaseSerializer("PulseSoundHold") { + override fun toAudioGroup(map: Map): PulseSoundHoldAudioGroup { + return PulseSoundHoldAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/PulseSoundHoldP2AudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/PulseSoundHoldP2AudioGroup.kt new file mode 100644 index 0000000..63bc4b8 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/PulseSoundHoldP2AudioGroup.kt @@ -0,0 +1,88 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable + +/** + * An audio group representing the sound when a hold for player 2 is pulsing. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(PulseSoundHoldP2AudioGroup.Serializer::class) +public class PulseSoundHoldP2AudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "PulseSoundHoldP2" + + /** + * The audio data of a hold pulse. + */ + public var pulseSoundHoldStartP2: SubGroupAudioData by this + + /** + * The audio data of the end of a short hold pulse. + */ + public var pulseSoundHoldShortEndP2: SubGroupAudioData by this + + /** + * The audio data of the end of a hold pulse. + */ + public var pulseSoundHoldEndP2: SubGroupAudioData by this + + /** + * The alternative audio data of a hold pulse. + */ + public var pulseSoundHoldStartAltP2: SubGroupAudioData by this + + /** + * The alternative audio data of the end of a short hold pulse. + */ + public var pulseSoundHoldShortEndAltP2: SubGroupAudioData by this + + /** + * The alternative audio data of the end of a hold pulse. + */ + public var pulseSoundHoldEndAltP2: SubGroupAudioData by this + + init { + // check existence + pulseSoundHoldStartP2 + pulseSoundHoldShortEndP2 + pulseSoundHoldEndP2 + pulseSoundHoldStartAltP2 + pulseSoundHoldShortEndAltP2 + pulseSoundHoldEndAltP2 + } + + /** + * Creates a [ClapSoundHoldAudioGroup] with the specified audio data. + */ + public constructor( + pulseSoundHoldStartP2: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldShortEndP2: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldEndP2: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldStartAltP2: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldShortEndAltP2: SubGroupAudioData = subGroupAudioDataOf(), + pulseSoundHoldEndAltP2: SubGroupAudioData = subGroupAudioDataOf(), + ) : this( + mapOf( + "PulseSoundHoldStartP2" to pulseSoundHoldStartP2, + "PulseSoundHoldShortEndP2" to pulseSoundHoldShortEndP2, + "PulseSoundHoldEndP2" to pulseSoundHoldEndP2, + "PulseSoundHoldStartAltP2" to pulseSoundHoldStartAltP2, + "PulseSoundHoldShortEndAltP2" to pulseSoundHoldShortEndAltP2, + "PulseSoundHoldEndAltP2" to pulseSoundHoldEndAltP2, + ) + ) + + public companion object Serializer : BaseSerializer("PulseSoundHoldP2") { + override fun toAudioGroup(map: Map): PulseSoundHoldP2AudioGroup { + return PulseSoundHoldP2AudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/SkipshotAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/SkipshotAudioGroup.kt new file mode 100644 index 0000000..03c8692 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/SkipshotAudioGroup.kt @@ -0,0 +1,45 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing the skipshot sound. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(SkipshotAudioGroup.Serializer::class) +public class SkipshotAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "Skipshot" + + /** + * The audio data of the sound. + */ + public var skipshot: SubGroupAudioData by this + + init { + skipshot // check existence + } + + /** + * Creates a [SkipshotAudioGroup] with the specified [skipshot] audio data. + */ + @JvmOverloads + public constructor(skipshot: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("Skipshot" to skipshot) + ) + + public companion object Serializer : BaseSerializer("Skipshot") { + override fun toAudioGroup(map: Map): SkipshotAudioGroup { + return SkipshotAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/SmallMistakeAudioGroup.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/SmallMistakeAudioGroup.kt new file mode 100644 index 0000000..9d22152 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/data/sound/audioGroup/SmallMistakeAudioGroup.kt @@ -0,0 +1,47 @@ +package cn.rdlevel.rdkt.core.data.sound.audioGroup + +import cn.rdlevel.rdkt.core.data.sound.SubGroupAudioData +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An audio group representing small mistake sound. + * + * @param subTypes A map of subtype identifiers to their corresponding [SubGroupAudioData]. + */ +@Serializable(SmallMistakeAudioGroup.Serializer::class) +public class SmallMistakeAudioGroup( + subTypes: Map = mapOf(), +) : AudioGroup(subTypes.toMutableMap()) { + override val soundType: String = "SmallMistake" + + /** + * The audio data of the sound. + */ + public var smallMistake: SubGroupAudioData by this + + init { + smallMistake // check existence + } + + /** + * Creates a [SmallMistakeAudioGroup] with the specified [smallMistake] audio data. + * + * @param smallMistake The audio data for the small mistake subtype. + */ + @JvmOverloads + public constructor(smallMistake: SubGroupAudioData = subGroupAudioDataOf()) : this( + mapOf("SmallMistake" to smallMistake) + ) + + public companion object Serializer : BaseSerializer("SmallMistake") { + override fun toAudioGroup(map: Map): SmallMistakeAudioGroup { + return SmallMistakeAudioGroup(map) + } + + init { + register() + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/CustomEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/CustomEvent.kt index cca02db..a941f3b 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/CustomEvent.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/CustomEvent.kt @@ -1,11 +1,13 @@ package cn.rdlevel.rdkt.core.events import cn.rdlevel.rdkt.core.RDLevel +import kotlinx.serialization.Serializable /** * An event that can be customized by the developers. * Developers are encouraged to use this interface to create their own custom events. */ +@Serializable public abstract class CustomEvent : AbstractEvent() { /** * Applies this event to the given level. diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/Event.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/Event.kt index 91fed97..438ecb1 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/Event.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/Event.kt @@ -42,6 +42,7 @@ public sealed interface Event { /** * An abstraction of an [Event] that provides default implementations for some properties. */ +@Serializable public sealed class AbstractEvent: Event { override var bar: Int = 1 set(value) { diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventProperties.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventProperties.kt index cf3ad74..2245929 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventProperties.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventProperties.kt @@ -1,5 +1,8 @@ package cn.rdlevel.rdkt.core.events +import kotlinx.serialization.SerialName +import kotlin.jvm.JvmStatic + /** * An [Event] that can specify a beat manually. */ @@ -30,4 +33,22 @@ public interface DurationSpecificEvent : Event { * The duration of the event in beats. */ public var duration: Double +} + +/** + * An [Event] that is specific to a single row. + */ +public interface RowSpecificEvent : Event { + /** + * The row id the event is specifying. + */ + @SerialName("row") + public var rowId: Int + + public companion object { + @JvmStatic + public fun requireRowInBound(rowId: Int) { + require(rowId in 0..15) { "Row id must be between 0 and 15." } + } + } } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventTabs.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventTabs.kt index 6f97eb2..8689e0b 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventTabs.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/EventTabs.kt @@ -1,28 +1,47 @@ package cn.rdlevel.rdkt.core.events +import kotlinx.serialization.Serializable + /** * An event that belongs to the sounds tab. */ +@Serializable public sealed class SoundEvent : AbstractEvent(), YSpecificEvent { override var y: Int = 0 } +/** + * A [SoundEvent] that is specific to a beat. + */ +@Serializable +public sealed class BeatSpecificSoundEvent : BeatSpecificEvent, SoundEvent() { + override var beat: Double = 1.0 + set(value) { + require(value >= 1) { "Beat must be greater than or equal to 1." } + field = value + } +} + /** * An event that belongs to the rows tab. */ +@Serializable public sealed class RowEvent : AbstractEvent() /** * An event that belongs to the actions tab. */ +@Serializable public sealed class ActionEvent : AbstractEvent() /** * An event that belongs to the rooms tab. */ +@Serializable public sealed class RoomEvent : AbstractEvent() /** * An event that belongs to the decorations tab. */ +@Serializable public sealed class DecorationEvent : AbstractEvent() \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/NarrateRowInfoEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/NarrateRowInfoEvent.kt new file mode 100644 index 0000000..6c42425 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/NarrateRowInfoEvent.kt @@ -0,0 +1,194 @@ +package cn.rdlevel.rdkt.core.events + + +import cn.rdlevel.rdkt.core.data.RowPattern +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlin.jvm.JvmStatic + +/** + * An event that narrates information about a specific row, such as when it is connected, updated, or disconnected. + * + * @property infoType The type of information to narrate (e.g., Connect, Update, Disconnect). + * @property soundOnly Whether to only play the narration sound without any additional information. + * @property skipsUnstable Whether the row being narrated has unstable row pattern that constantly changed. Will be ignored if [infoType] is [InfoType.DISCONNECT]. + * @property customPlayer The custom player for which the narration is intended. + */ +@Serializable +@SerialName("NarrateRowInfo") +public class NarrateRowInfoEvent private constructor( + public var infoType: InfoType = InfoType.CONNECT, + public var soundOnly: Boolean = false, + private var narrateSkipBeats: String = "On", + public var skipsUnstable: Boolean = false, + private var customPattern: RowPattern? = null, + public var customPlayer: CustomPlayer = CustomPlayer.AUTO_DETECT, +) : BeatSpecificSoundEvent(), RowSpecificEvent { + @SerialName("row") + override var rowId: Int = 0 + set(value) { + RowSpecificEvent.requireRowInBound(value) + field = value + } + + @Transient + private var _rowPattern: RowPatternNarration = RowPatternNarration.On + + /** + * The pattern narration setting for the row. + * Will be ignored if [infoType] is [InfoType.DISCONNECT]. + */ + public var rowPattern: RowPatternNarration + get() = _rowPattern + set(value) { + narrateSkipBeats = value.type + customPattern = (value as? RowPatternNarration.Custom)?.pattern + _rowPattern = value + } + + private fun initSkipPattern() { + _rowPattern = when (narrateSkipBeats) { + "On" -> RowPatternNarration.On + "Off" -> RowPatternNarration.Off + RowPatternNarration.Custom.TYPE -> customPattern?.let { RowPatternNarration.Custom(it) } + ?: error("Expected customPattern to be non-null when narrateSkipBeats is 'Custom'.") + + else -> error("Invalid narrateSkipBeats value: $narrateSkipBeats") + } + } + + /** + * The length of the row to be narrated when using a custom row pattern. Must be between 1 and 7. Setting this to null will use the default row length for narration. + */ + public var customRowLength: Int? = null + set(value) { + if (value != null) { + require(value in 1..7) { "Row length must be between 1 and 7." } + } + field = value + } + + init { + initSkipPattern() + } + + public companion object { + /** + * Creates a copy of the given [NarrateRowInfoEvent] with the same properties. + */ + @JvmStatic + public fun of(event: NarrateRowInfoEvent): NarrateRowInfoEvent { + return NarrateRowInfoEvent( + infoType = event.infoType, + soundOnly = event.soundOnly, + narrateSkipBeats = event.narrateSkipBeats, + skipsUnstable = event.skipsUnstable, + customPattern = event.customPattern, + customPlayer = event.customPlayer, + ).apply { + rowId = event.rowId + customRowLength = event.customRowLength + } + } + + /** + * Creates a new [NarrateRowInfoEvent] with the properties set by the given block. + */ + @JvmStatic + public fun of(block: NarrateRowInfoEvent.() -> Unit): NarrateRowInfoEvent { + return NarrateRowInfoEvent().apply(block) + } + } + + /** + * Represents how the row pattern is narrated. + */ + @Serializable + public sealed class RowPatternNarration(public val type: String) { + /** + * Narrate the row pattern as it is. + */ + @Serializable + @SerialName("On") + public object On : RowPatternNarration("On") + + /** + * Do not narrate the row pattern. + */ + @Serializable + @SerialName("Off") + public object Off : RowPatternNarration("Off") + + /** + * Narrate the row pattern with the provided pattern. + * + * @property pattern The custom row pattern to be narrated. + */ + @Serializable + @SerialName("Custom") + public class Custom(public val pattern: RowPattern) : RowPatternNarration(TYPE) { + public companion object { + public const val TYPE: String = "Custom" + } + } + } + + /** + * Represents the type of row information to be narrated. + */ + @Serializable + public enum class InfoType { + /** + * The row is being connected. + */ + @SerialName("Connect") + CONNECT, + + /** + * The row is being updated. + */ + @SerialName("Update") + UPDATE, + + /** + * The row is being disconnected. + */ + @SerialName("Disconnect") + DISCONNECT, + + /** + * The cpu for the row is being online. + */ + @SerialName("Online") + ONLINE, + + /** + * The cpu for the row is being offline. + */ + @SerialName("Offline") + OFFLINE, + } + + /** + * Represents the custom player for which the narration is intended. + */ + @Serializable + public enum class CustomPlayer { + /** + * The player to narrate will be automatically detected. + */ + @SerialName("AutoDetect") + AUTO_DETECT, + + /** + * The player to narrate is Player 1. + */ + P1, + + /** + * The player to narrate is Player 2. + */ + P2, + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/PlaySongEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/PlaySongEvent.kt new file mode 100644 index 0000000..8f40372 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/PlaySongEvent.kt @@ -0,0 +1,43 @@ +package cn.rdlevel.rdkt.core.events + + +import cn.rdlevel.rdkt.core.data.sound.AudioData +import cn.rdlevel.rdkt.core.data.sound.audioDataOfMusic +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * Plays a song in the level. + * + * @property song The song to play. + */ +@Serializable +@SerialName("PlaySong") +public class PlaySongEvent( + public var song: AudioData, +) : SoundEvent() { + /** + * The beats per minute (BPM) of the song. + */ + public var bpm: Double = 100.0 + set(value) { + require(value >= 1) { "BPM must be at least 1." } + field = value + } + + /** + * Whether the song should loop. + */ + public var loop: Boolean = false + + /** + * Creates a new [PlaySongEvent] with the specified [fileName], [bpm], and [offset]. + */ + @JvmOverloads + public constructor(fileName: String = "sndOrientalTechno", bpm: Double = 100.0, offset: Int = 0) : this( + audioDataOfMusic(fileName, offset) + ) { + this.bpm = bpm + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/PlaySoundEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/PlaySoundEvent.kt new file mode 100644 index 0000000..14c577d --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/PlaySoundEvent.kt @@ -0,0 +1,56 @@ +package cn.rdlevel.rdkt.core.events + + +import cn.rdlevel.rdkt.core.data.sound.AudioData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * Plays a sound. + */ +@Serializable +@SerialName("PlaySound") +public class PlaySoundEvent( + /** + * The sound to play. + */ + public var sound: AudioData, + /** + * The type of sound. + */ + public var customSoundType: SoundType = SoundType.CUE_SOUND, +) : BeatSpecificSoundEvent() { + /** + * The type of the sound. + */ + @Serializable + public enum class SoundType { + @SerialName("CueSound") + CUE_SOUND, + + @SerialName("MusicSound") + MUSIC_SOUND, + + @SerialName("BeatSound") + BEAT_SOUND, + + @SerialName("HitSound") + HIT_SOUND, + + @SerialName("OtherSound") + OTHER_SOUND, + } + + /** + * Creates a new [PlaySoundEvent] with the specified [fileName] and [customSoundType]. + * + * @param fileName The name of the sound file. + * @param customSoundType The type of sound. + */ + @JvmOverloads + public constructor(fileName: String = "Shaker", customSoundType: SoundType = SoundType.CUE_SOUND) : this( + AudioData(fileName), + customSoundType + ) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/ReadNarrationEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/ReadNarrationEvent.kt new file mode 100644 index 0000000..04ecaa9 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/ReadNarrationEvent.kt @@ -0,0 +1,35 @@ +package cn.rdlevel.rdkt.core.events + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * An event that reads the specified text as a narration. + * + * @property text The text to be narrated. + * @property category The category of the narration. + */ +@Serializable +@SerialName("ReadNarration") +public data class ReadNarrationEvent @JvmOverloads constructor( + var text: String, + var category: Category = Category.DESCRIPTION, +) : BeatSpecificSoundEvent() { + /** + * Represents the category of the narration. + */ + public enum class Category { + @SerialName("Notification") + NOTIFICATION, + + @SerialName("Description") + DESCRIPTION, + + @SerialName("Subtitles") + SUBTITLES, + + @SerialName("Instruction") + INSTRUCTION, + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SayReadyGetSetGoEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SayReadyGetSetGoEvent.kt new file mode 100644 index 0000000..34dc36d --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SayReadyGetSetGoEvent.kt @@ -0,0 +1,193 @@ +package cn.rdlevel.rdkt.core.events + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * Event that gives players cues from the nurse for the rows. + * You should use this event to cue oneshot rows. + */ +@Serializable +@SerialName("SayReadyGetSetGo") +public class SayReadyGetSetGoEvent @JvmOverloads constructor( + /** + * The phrase to be spoken by the nurse. + */ + public var phraseToSay: NursePhraseType, + /** + * The source of the voice for the nurse. + */ + public var voiceSource: NurseVoiceType = NurseVoiceType.NURSE, +) { + /** + * The delay between each syllable spoken by the nurse in beats. + * Setting this to 0 or lower will cause the nurse to speak immediately. + * When the phrase only has one syllable, this value is ignored. + */ + public var tick: Double = 1.0 + + /** + * The volume of the voice. + * This value must be between 0 and 200. + */ + public var volume: Int = 100 + set(value) { + require(value in 0..200) { "Volume must be between 0 and 200." } + field = value + } + + /** + * Constructs a SayReadyGetSetGoEvent with the specified phrase, voice source, tick, and volume. + * + * @param phraseToSay The phrase to be spoken by the nurse. + * @param voiceSource The source of the voice for the nurse. + * @param tick The delay between each syllable spoken by the nurse in beats. Defaults to 1.0. + * @param volume The volume of the voice, must be between 0 and 200. Defaults to 100. + */ + public constructor( + phraseToSay: NursePhraseType, + voiceSource: NurseVoiceType, + tick: Double = 1.0, + volume: Int = 100 + ) : this( + phraseToSay, + voiceSource + ) { + this.tick = tick + this.volume = volume + } + + /** + * Enum representing the different types of phrases that can be spoken by the nurse. + */ + @Serializable + public enum class NursePhraseType { + @SerialName("JustSayRea") + REA, + + @SerialName("JustSayDy") + DY, + + @SerialName("JustSayGet") + GET, + + @SerialName("JustSaySet") + SET, + + @SerialName("JustSayGo") + GO, + + @SerialName("JustSayReady") + READY, + + @SerialName("JustSayAnd") + AND, + + @SerialName("JustSayStop") + STOP, + + @SerialName("JustSayAndStop") + AND_STOP, + + @SerialName("SaySwitch") + SWITCH, + + @SerialName("SayWatch") + WATCH, + + @SerialName("SayListen") + LISTEN, + + @SerialName("SayGetSetGo") + GET_SET_GO, + + /** + * The old version of [REA_DY_GET_SET_GO]. + * Avoid using this to cue oneshot rows. + */ + @SerialName("SayReadyGetSetGo") + READY_GET_SET_GO, + + /** + * The new version of [REA_DY_GET_SET_GO]. + */ + @SerialName("SayReaDyGetSetGoNew") + REA_DY_GET_SET_GO, + + @SerialName("SayGetSetOne") + GET_SET_ONE, + + @SerialName("SayReaDyGetSetOne") + REA_DY_GET_SET_ONE, + + @SerialName("Count1") + COUNT1, + + @SerialName("Count2") + COUNT2, + + @SerialName("Count3") + COUNT3, + + @SerialName("Count4") + COUNT4, + + @SerialName("Count5") + COUNT5, + + @SerialName("Count6") + COUNT6, + + @SerialName("Count7") + COUNT7, + + @SerialName("Count8") + COUNT8, + + @SerialName("Count9") + COUNT9, + + @SerialName("Count10") + COUNT10, + } + + /** + * Enum representing the different voice types for the nurse. + */ + @Serializable + public enum class NurseVoiceType { + @SerialName("Nurse") + NURSE, + + @SerialName("NurseTired") + NURSE_TIRED, + + @SerialName("NurseSwing") + NURSE_SWING, + + @SerialName("NurseSwingCalm") + NURSE_SWING_CALM, + + @SerialName("IanExcited") + IAN_EXCITED, + + @SerialName("IanCalm") + IAN_CALM, + + @SerialName("IanSlow") + IAN_SLOW, + + /** + * No voice, only bottom lights. + */ + @SerialName("NoneBottom") + NONE_BOTTOM, + + /** + * No voice, only top lights. + */ + @SerialName("NoneTop") + NONE_TOP + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetBeatSoundEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetBeatSoundEvent.kt new file mode 100644 index 0000000..369adcc --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetBeatSoundEvent.kt @@ -0,0 +1,26 @@ +package cn.rdlevel.rdkt.core.events + +import cn.rdlevel.rdkt.core.data.sound.AudioData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * An event that sets the pulse sound for a specific row. + * + * @property sound The pulse sound [AudioData] to set for the specified row. + */ +@Serializable +public data class SetBeatSoundEvent( + var sound: AudioData, +) : BeatSpecificSoundEvent(), RowSpecificEvent { + @SerialName("row") + override var rowId: Int = 0 + set(value) { + RowSpecificEvent.requireRowInBound(value) + field = value + } + + public constructor(rowId: Int, sound: AudioData) : this(sound) { + this.rowId = rowId + } +} diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetBeatsPerMinuteEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetBeatsPerMinuteEvent.kt new file mode 100644 index 0000000..940f76d --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetBeatsPerMinuteEvent.kt @@ -0,0 +1,31 @@ +package cn.rdlevel.rdkt.core.events + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Sets the BPM (Beats Per Minute) for the level. + * Note that this will override the BPM in [PlaySongEvent]. + */ +@Serializable +@SerialName("SetBeatsPerMinute") +public class SetBeatsPerMinuteEvent() : BeatSpecificSoundEvent() { + /** + * The beats per minute for the level. + */ + public var beatsPerMinute: Double = 100.0 + set(value) { + require(value >= 1) { "BPM must be at least 1." } + field = value + } + + /** + * Creates a new [SetBeatsPerMinuteEvent] with the specified BPM. + * + * @param bpm The beats per minute for the level. + */ + public constructor(bpm: Double = 100.0) : this() { + beatsPerMinute = bpm + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetClapSoundsEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetClapSoundsEvent.kt new file mode 100644 index 0000000..8572159 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetClapSoundsEvent.kt @@ -0,0 +1,61 @@ +package cn.rdlevel.rdkt.core.events + + +import cn.rdlevel.rdkt.core.data.sound.AudioData +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * Sets the clap sounds for the players and CPU in the level. + */ +@Serializable +@SerialName("SetClapSounds") +public class SetClapSoundsEvent( + /** + * The type of rows this event applies to. + */ + public var rowType: RowType = RowType.CLASSIC, + public var p1Sound: AudioData? = null, + public var p2Sound: AudioData? = null, + public var cpuSound: AudioData? = null, +) : BeatSpecificSoundEvent() { + /** + * The type of rows. + */ + @Serializable + public enum class RowType { + /** + * Classic row. + */ + @SerialName("Classic") + CLASSIC, + + /** + * Oneshot rows. + */ + @SerialName("Oneshot") + ONESHOT, + } + + /** + * Creates a new SetClapSoundsEvent with the specified row type and sound file names. + * + * @param rowType The type of rows this event applies to. + * @param p1SoundFileName The sound file name for player 1. + * @param p2SoundFileName The sound file name for player 2. + * @param cpuSoundFileName The sound file name for the CPU. + */ + @JvmOverloads + public constructor( + rowType: RowType = RowType.CLASSIC, + p1SoundFileName: String? = null, + p2SoundFileName: String? = null, + cpuSoundFileName: String? = null, + ) : this( + rowType, + p1SoundFileName?.let { AudioData(it) }, + p2SoundFileName?.let { AudioData(it) }, + cpuSoundFileName?.let { AudioData(it) }, + ) +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetCountingSoundEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetCountingSoundEvent.kt new file mode 100644 index 0000000..63f4f3d --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetCountingSoundEvent.kt @@ -0,0 +1,137 @@ +package cn.rdlevel.rdkt.core.events + + +import cn.rdlevel.rdkt.core.data.sound.AudioData +import cn.rdlevel.rdkt.core.data.sound.VoiceSource +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import kotlin.jvm.JvmOverloads +import kotlin.jvm.JvmStatic + +/** + * An event that sets the counting sound for a specific row. + */ +@Serializable +@SerialName("SetCountingSound") +public class SetCountingSoundEvent private constructor( + @SerialName("enabled") + private var _enabled: Boolean, + @SerialName("voiceSource") + private var voiceSourceType: String? = null, + private var sounds: Array? = null, +) : BeatSpecificSoundEvent(), RowSpecificEvent { + @SerialName("row") + override var rowId: Int = 0 + set(value) { + RowSpecificEvent.requireRowInBound(value) + field = value + } + + @Transient + private var _voiceSource: VoiceSource? = null + + /** + * The voice source for the counting sound. Setting this to null will disable the counting sound for the specified row. + */ + public var voiceSource: VoiceSource? + get() = _voiceSource + set(value) { + _enabled = value != null + voiceSourceType = value?.type + sounds = (value as? VoiceSource.Custom)?.sounds + _voiceSource = value + } + + private fun initVoiceSource() { + _voiceSource = when (voiceSourceType) { + null -> null + VoiceSource.Custom.TYPE -> VoiceSource.Custom(sounds ?: error("Missing sounds for custom voice source.")) + else -> VoiceSource.fromType(voiceSourceType!!) ?: error("Unknown voice source type: $voiceSourceType") + } + } + + /** + * The volume of the counting sound, between 0 and 100. + */ + public var volume: Int = 100 + set(value) { + require(value in 0..100) { "Volume must be between 0 and 100." } + field = value + } + + /** + * The offset of the counting sound for oneshot row, between 0.0 and 1.0. + * This is ignored for classic row. + */ + @SerialName("subdivOffset") + public var subdivisionOffset: Double = 0.5 + set(value) { + require(value in 0.0..1.0) { "Subdivision offset must be between 0.0 and 1.0." } + field = value + } + + init { + initVoiceSource() + } + + public companion object { + /** + * Creates a copy of the given [SetCountingSoundEvent]. + */ + @JvmStatic + public fun of(event: SetCountingSoundEvent): SetCountingSoundEvent { + return SetCountingSoundEvent(event._enabled).apply { + this.rowId = event.rowId + this.voiceSource = event.voiceSource + this.volume = event.volume + this.subdivisionOffset = event.subdivisionOffset + } + } + + /** + * Creates a [SetCountingSoundEvent] for a classic row with the given parameters. + * + * @param rowId The row id to set the counting sound for. + * @param voiceSource The voice source to set for the counting sound. Setting this to null will disable the counting sound for the specified row. + * @param volume The volume of the counting sound, between 0 and 100. Default is 100. + */ + @JvmStatic + @JvmOverloads + public fun ofClassic( + rowId: Int, + voiceSource: VoiceSource.Classic?, + volume: Int = 100, + ): SetCountingSoundEvent { + return SetCountingSoundEvent(false).apply { + this.rowId = rowId + this.voiceSource = voiceSource + this.volume = volume + } + } + + /** + * Creates a [SetCountingSoundEvent] for a oneshot row with the given parameters. + * + * @param rowId The row id to set the counting sound for. + * @param voiceSource The voice source to set for the counting sound. Setting this to null will disable the counting sound for the specified row. + * @param volume The volume of the counting sound, between 0 and 100. Default is 100. + * @param subdivisionOffset The offset of the counting sound for oneshot row, between 0.0 and 1.0. Default is 0.5. + */ + @JvmStatic + @JvmOverloads + public fun ofOneshot( + rowId: Int, + voiceSource: VoiceSource.Oneshot?, + volume: Int = 100, + subdivisionOffset: Double = 0.5, + ): SetCountingSoundEvent { + return SetCountingSoundEvent(false).apply { + this.rowId = rowId + this.voiceSource = voiceSource + this.volume = volume + this.subdivisionOffset = subdivisionOffset + } + } + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetCrotchetsPerBarEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetCrotchetsPerBarEvent.kt new file mode 100644 index 0000000..508cda6 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetCrotchetsPerBarEvent.kt @@ -0,0 +1,36 @@ +package cn.rdlevel.rdkt.core.events + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Sets the number of beats per bar and the visual beat multiplier. + */ +@Serializable +@SerialName("SetCrotchetsPerBar") +public class SetCrotchetsPerBarEvent() : SoundEvent() { + /** + * The beats per bar until the next same event that overrides this one. + */ + public var crotchetsPerBar: Int = 8 + set(value) { + require(value > 0) { "Crotchets per bar must be greater than 0." } + field = value + } + + /** + * Controls the speed of some sprite animations. + * The animations will be played with 4 / [visualBeatMultiplier] frames per beat. + */ + public var visualBeatMultiplier: Double = 1.0 + set(value) { + require(value >= 0) { "Visual beat multiplier must be non-negative." } + field = value + } + + public constructor(crotchetsPerBar: Int = 8, visualBeatMultiplier: Double = 1.0) : this() { + this.crotchetsPerBar = crotchetsPerBar + this.visualBeatMultiplier = visualBeatMultiplier + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetGameSoundEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetGameSoundEvent.kt new file mode 100644 index 0000000..014ddfa --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetGameSoundEvent.kt @@ -0,0 +1,102 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package cn.rdlevel.rdkt.core.events + +import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI +import cn.rdlevel.rdkt.core.data.sound.audioGroup.AudioGroup +import cn.rdlevel.rdkt.core.data.sound.audioGroup.tryConvertTo +import cn.rdlevel.rdkt.core.serialization.TransformSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KeepGeneratedSerializer +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* + +/** + * An event that sets the configurations for a specific group of game sounds. + * + * This event allows you to define and modify the audio settings for a particular + * sound group identified by its type. The configurations are encapsulated within + * an [AudioGroup] instance. + * + * @property soundType The type of sound group. + * @property group The audio group configurations associated with this event. + */ +@ConsistentCopyVisibility +@Serializable(SetGameSoundEvent.Serializer::class) +@KeepGeneratedSerializer +@SerialName("SetGameSound") +public data class SetGameSoundEvent private constructor( + @SerialName("soundType") + private var _soundType: String, + @SerialName("soundSubtypes") + private var _group: AudioGroup, +) : BeatSpecificSoundEvent() { + /** + * Creates a [SetGameSoundEvent] with the specified [AudioGroup]. + * + * @param group The audio group configurations to be associated with this event. + */ + public constructor(group: AudioGroup) : this(group.soundType, group) + + /** + * The type of sound group. + * + * This property is immutable, reflecting the [AudioGroup.soundType] of the associated [AudioGroup]. + * + * @see AudioGroup.soundType + */ + val soundType: String + get() = _group.soundType + + /** + * The audio group configurations associated with this event. + */ + var group: AudioGroup + get() = _group + set(value) { + _soundType = value.soundType + _group = value + } + + override fun toString(): String { + return "SetGameSoundEvent(soundType='$soundType', group=$group)" + } + + public object Serializer : JsonTransformingSerializer(DataSerializer) { + @OptIn(RDKTInternalAPI::class) + private object DataSerializer : + TransformSerializer(generatedSerializer()) { + override fun toData(value: SetGameSoundEvent) = value + + override fun fromData(data: SetGameSoundEvent): SetGameSoundEvent { + data._group = data._group.tryConvertTo(data._soundType) + return data + } + } + + override fun transformDeserialize(element: JsonElement): JsonElement { + require(element is JsonObject) { "Expected JsonObject for SetGameSoundEvent deserialization, but got $element." } + val content = element.toMutableMap() + content.remove("type") + + if ("soundSubtypes" in element) { + return JsonObject(content) + } + + val subGroupAudioDataElement = buildJsonObject { + put( + "groupSubtype", + element["soundType"] ?: error("Missing 'soundType' field in SetGameSoundEvent JSON.") + ) + put("used", true) + listOf("used", "filename", "volume", "pitch", "pan", "offset").forEach { key -> + element[key]?.let { put(key, it) } + content.remove(key) + } + } + content["soundSubtypes"] = JsonArray(listOf(subGroupAudioDataElement)) + return JsonObject(content) + } + } +} diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetHeartExplodeIntervalEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetHeartExplodeIntervalEvent.kt new file mode 100644 index 0000000..ca729a5 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetHeartExplodeIntervalEvent.kt @@ -0,0 +1,58 @@ +package cn.rdlevel.rdkt.core.events + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.jvm.JvmOverloads + +/** + * Set how the heart explosion interval is calculated after hitting a beat correctly. + */ +@Serializable +@SerialName("SetHeartExplodeInterval") +public class SetHeartExplodeIntervalEvent @JvmOverloads constructor( + /** + * The type of interval calculation. + */ + public var intervalType: HeartExplosionIntervalType = HeartExplosionIntervalType.COMBINE_ON_DOWNBEAT, + /** + * The interval in beats. + */ + public var interval: Double = 1.0, +) { + /** + * The type of interval calculation for heart explosions. + */ + @Serializable + public enum class HeartExplosionIntervalType { + /** + * The explosion will occur after the specified interval. + */ + @SerialName("OneBeatAfter") + FIXED_INTERVAL, + + /** + * The explosions will be combined and rounded up by the specified interval. + */ + @SerialName("GatherAndCeil") + COMBINE_ON_DOWNBEAT, + + /** + * The explosions will be combined by the specified interval. + */ + @SerialName("GatherNoCeil") + COMBINE_ON_FIXED_INTERVAL, + + /** + * The explosion will occur immediately without sound. + */ + @SerialName("Instant") + INSTANT, + + /** + * The explosion will be disabled. + */ + @SerialName("Disabled") + DISABLED + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetHeartExplodeVolumeEvent.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetHeartExplodeVolumeEvent.kt new file mode 100644 index 0000000..51d959f --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/events/SetHeartExplodeVolumeEvent.kt @@ -0,0 +1,31 @@ +package cn.rdlevel.rdkt.core.events + + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Set the volume of the heart explode sound when hitting a beat correctly. + */ +@Serializable +@SerialName("SetHeartExplodeVolume") +public class SetHeartExplodeVolumeEvent() : SoundEvent() { + /** + * The volume of the heart explode sound. + * Must be greater than or equal to 0. + */ + public var volume: Int = 60 + set(value) { + require(value >= 0) { "Volume must be greater than or equal to 0." } + field = value + } + + /** + * Creates a new SetHeartExplodeVolumeEvent with the specified volume. + * + * @param volume The volume of the heart explode sound. + */ + public constructor(volume: Int = 60) : this() { + this.volume = volume + } +} \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/PolymorphicDelegatedSerializer.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/PolymorphicDelegatedSerializer.kt similarity index 80% rename from core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/PolymorphicDelegatedSerializer.kt rename to core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/PolymorphicDelegatedSerializer.kt index f66781c..14ae899 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/PolymorphicDelegatedSerializer.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/PolymorphicDelegatedSerializer.kt @@ -1,4 +1,4 @@ -package cn.rdlevel.rdkt.core.serializers +package cn.rdlevel.rdkt.core.serialization import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI import kotlinx.serialization.KSerializer @@ -32,10 +32,16 @@ public abstract class PolymorphicDelegatedSerializer( override fun deserialize(decoder: Decoder): Sub { val result = decoder.decodeSerializableValue(baseSerializer) - runCatching { + val throwable = runCatching { @Suppress("UNCHECKED_CAST") return result as Sub + }.exceptionOrNull() + if (throwable is ClassCastException) { + throw RuntimeException( + "Unable to cast deserialized value to specified type. Check your base serializer.", + throwable + ) } - error("Unable to cast deserialized value to specified type. Check your base serializer.") + throw throwable!! } } \ No newline at end of file diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/StringListAsStringSerializer.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/StringListAsStringSerializer.kt similarity index 97% rename from core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/StringListAsStringSerializer.kt rename to core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/StringListAsStringSerializer.kt index 478679a..95a3fc6 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/StringListAsStringSerializer.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/StringListAsStringSerializer.kt @@ -1,6 +1,6 @@ @file:OptIn(RDKTInternalAPI::class) -package cn.rdlevel.rdkt.core.serializers +package cn.rdlevel.rdkt.core.serialization import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI import kotlinx.serialization.KSerializer diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/TransformSerializer.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/TransformSerializer.kt similarity index 94% rename from core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/TransformSerializer.kt rename to core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/TransformSerializer.kt index e3139f2..9a72977 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serializers/TransformSerializer.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/serialization/TransformSerializer.kt @@ -1,4 +1,4 @@ -package cn.rdlevel.rdkt.core.serializers +package cn.rdlevel.rdkt.core.serialization import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI import kotlinx.serialization.KSerializer @@ -16,6 +16,7 @@ import kotlinx.serialization.encoding.Encoder * @param Data The data representation of the type. */ @RDKTInternalAPI +@SubclassOptInRequired(RDKTInternalAPI::class) public abstract class TransformSerializer( private val dataSerializer: KSerializer ) : KSerializer { diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/settings/LevelSettings.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/settings/LevelSettings.kt index 7e3d031..f205eba 100644 --- a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/settings/LevelSettings.kt +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/settings/LevelSettings.kt @@ -3,7 +3,7 @@ package cn.rdlevel.rdkt.core.settings import cn.rdlevel.rdkt.core.annotations.RDKTInternalAPI -import cn.rdlevel.rdkt.core.serializers.MutableStringListSerializedInString +import cn.rdlevel.rdkt.core.serialization.MutableStringListSerializedInString import cn.rdlevel.rdkt.core.settings.LevelSettings.Companion.CURRENT_LEVEL_VERSION import kotlinx.serialization.Serializable import kotlin.jvm.JvmName @@ -16,7 +16,7 @@ import kotlin.random.Random @Serializable public class LevelSettings { /** - * The version of the level. Currently, it is set to [63][CURRENT_LEVEL_VERSION]. + * The version of the level. Currently, it is set to [64][CURRENT_LEVEL_VERSION]. * * This library currently does not support levels with other versions. * If you want to use levels with other versions, please use the level editor to convert them to a supported version. @@ -197,7 +197,7 @@ public class LevelSettings { /** * The current level version the level editor is using. */ - public const val CURRENT_LEVEL_VERSION: Int = 63 + public const val CURRENT_LEVEL_VERSION: Int = 64 } } diff --git a/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/util/Text.kt b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/util/Text.kt new file mode 100644 index 0000000..cbea273 --- /dev/null +++ b/core/src/commonMain/kotlin/cn/rdlevel/rdkt/core/util/Text.kt @@ -0,0 +1,21 @@ +package cn.rdlevel.rdkt.core.util + +/** + * Converts a BigCamelCase string to smallCamelCase. + * + * @return The converted smallCamelCase string. + */ +public fun String.bigCamelToSmallCamel(): String { + if (this.isEmpty()) return this + return this[0].lowercaseChar() + this.substring(1) +} + +/** + * Converts a smallCamelCase string to BigCamelCase. + * + * @return The converted BigCamelCase string. + */ +public fun String.smallCamelToBigCamel(): String { + if (this.isEmpty()) return this + return this[0].uppercaseChar() + this.substring(1) +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/NarrateRowInfoEventTest.kt b/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/NarrateRowInfoEventTest.kt new file mode 100644 index 0000000..b6861ea --- /dev/null +++ b/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/NarrateRowInfoEventTest.kt @@ -0,0 +1,33 @@ +package cn.rdlevel.rdkt.core.test + +import cn.rdlevel.rdkt.core.data.RowPattern +import cn.rdlevel.rdkt.core.events.Event +import cn.rdlevel.rdkt.core.events.NarrateRowInfoEvent +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlin.test.Test + +class NarrateRowInfoEventTest { + @OptIn(ExperimentalSerializationApi::class) + @Test + fun test() { + val json = Json { + isLenient = true + allowTrailingComma = true + encodeDefaults = true + } + val event: Event = NarrateRowInfoEvent.of { + infoType = NarrateRowInfoEvent.InfoType.ONLINE + rowPattern = NarrateRowInfoEvent.RowPatternNarration.Custom(RowPattern("-x-x-x")) + customPlayer = NarrateRowInfoEvent.CustomPlayer.P2 + customRowLength = 5 + } + val jsonText = json.encodeToString(event) + println(jsonText) + + val jsonText2 = + """{ "bar": 1, "beat": 1, "y": 0, "type": "NarrateRowInfo", "row": 0, "infoType": "Online", "soundOnly": false, "narrateSkipBeats": "Custom", "customPattern": "-xudbr", "customPlayer": "AutoDetect", "customRowLength": 7 }""" + val event2: Event = json.decodeFromString(jsonText2) + println(event2) + } +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/SetCountingSoundEventTest.kt b/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/SetCountingSoundEventTest.kt new file mode 100644 index 0000000..be44b7e --- /dev/null +++ b/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/SetCountingSoundEventTest.kt @@ -0,0 +1,36 @@ +package cn.rdlevel.rdkt.core.test + +import cn.rdlevel.rdkt.core.data.sound.VoiceSource +import cn.rdlevel.rdkt.core.events.Event +import cn.rdlevel.rdkt.core.events.SetCountingSoundEvent +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlin.test.Test + +class SetCountingSoundEventTest { + @OptIn(ExperimentalSerializationApi::class) + @Test + fun test() { + val json = Json { + isLenient = true + allowTrailingComma = true + encodeDefaults = true + } + + val event: Event = SetCountingSoundEvent.ofClassic(0, null) + val event2: Event = SetCountingSoundEvent.ofOneshot(1, VoiceSource.Oneshot.IanCountEnglish) + + val jsonText = json.encodeToString(event) + println(jsonText) + val jsonText2 = json.encodeToString(event2) + println(jsonText2) + + val event3: Event = json.decodeFromString(jsonText2) + println(event3) + + val jsonText4 = + """{ "bar": 1, "beat": 1, "y": 0, "type": "SetCountingSound", "row": 1, "enabled": true, "voiceSource": "Custom", "volume": 100, "sounds": [{"filename": "Jyi - ChineseCount1"}, {"filename": "Jyi - ChineseCount2"}, {"filename": "Jyi - ChineseCount3"}, {"filename": "Jyi - ChineseCount4"}, {"filename": "Jyi - ChineseCount5"}, {"filename": "Jyi - ChineseCount6"}, {"filename": "Jyi - ChineseCount7"}] }""" + val event4: Event = json.decodeFromString(jsonText4) + println(event4) + } +} \ No newline at end of file diff --git a/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/SetGameSoundEventTest.kt b/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/SetGameSoundEventTest.kt new file mode 100644 index 0000000..6af86c1 --- /dev/null +++ b/core/src/commonTest/kotlin/cn/rdlevel/rdkt/core/test/SetGameSoundEventTest.kt @@ -0,0 +1,37 @@ +package cn.rdlevel.rdkt.core.test + +import cn.rdlevel.rdkt.core.data.sound.audioGroup.SmallMistakeAudioGroup +import cn.rdlevel.rdkt.core.data.sound.subGroupAudioDataOf +import cn.rdlevel.rdkt.core.events.Event +import cn.rdlevel.rdkt.core.events.SetGameSoundEvent +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlin.test.Test + +class SetGameSoundEventTest { + @OptIn(ExperimentalSerializationApi::class) + @Test + fun test() { + val json = Json { + isLenient = true + allowTrailingComma = true + encodeDefaults = true + } + + val event: Event = SetGameSoundEvent( + SmallMistakeAudioGroup(subGroupAudioDataOf("bar")) + ).apply { + bar = 114 + y = 514 + } + val jsonText = json.encodeToString(event) + println(jsonText) + + val event2: Event = json.decodeFromString(jsonText) + println(event2) + + val event3: Event = + json.decodeFromString("""{ "bar": 1, "beat": 1, "y": 0, "type": "SetGameSound", "soundType": "SmallMistake", "filename": "" }""") + println(event3) + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 38478b1..782561d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] agp = "8.5.2" -kotlin = "2.1.10" +kotlin = "2.3.0" android-minSdk = "24" android-compileSdk = "34"