Implemented sound events.

This commit is contained in:
2026-03-03 01:43:14 +08:00
parent 2ffe765f4e
commit 5d2a385d3c
47 changed files with 2769 additions and 16 deletions
+2 -2
View File
@@ -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 {
+2 -2
View File
@@ -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 {
@@ -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<Pulse>,
) {
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<Pulse>,
) {
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<RowPattern, String>(String.serializer()) {
override fun toData(value: RowPattern): String {
return value.pattern
}
override fun fromData(data: String): RowPattern {
return RowPattern(data)
}
}
}
@@ -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<SelectedRoomsAndTopLayer, Set<Int>>(SetSerializer(Int.serializer())) {
TransformSerializer<SelectedRoomsAndTopLayer, Set<Int>>(kotlinx.serialization.serializer()) {
override fun toData(value: SelectedRoomsAndTopLayer): Set<Int> {
return value.rooms
}
@@ -403,6 +403,7 @@ public fun <T : SingleSelectedRoomOrTopLayer> singleRoomOf(kClass: KClass<T>, 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 <reified T : SelectedRoomsAndTopLayer> roomsOf(rooms: Set<Int>): T {
return roomsOf(T::class, rooms)
}
@@ -415,6 +416,7 @@ public inline fun <reified T : SelectedRoomsAndTopLayer> roomsOf(rooms: Set<Int>
* @param rooms Additional room IDs to include in the selection.
* @return An instance of [SelectedRoomsAndTopLayer] of the specified type.
*/
@JvmName("of")
public inline fun <reified T : SelectedRoomsAndTopLayer> roomsOf(room: Int, vararg rooms: Int): T {
return roomsOf(T::class, room, *rooms)
}
@@ -426,6 +428,7 @@ public inline fun <reified T : SelectedRoomsAndTopLayer> 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 <reified T : SingleSelectedRoomOrTopLayer> singleRoomOf(room: Int): T {
return singleRoomOf(T::class, room)
}
@@ -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)
@@ -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<AudioData>,
) : 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<AudioData>): 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<AudioData>): 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<String, VoiceSource> = 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]
}
}
}
@@ -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<String, SubGroupAudioData> = mutableMapOf(),
) : Map<String, SubGroupAudioData> 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<String, (Map<String, SubGroupAudioData>) -> 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<String, SubGroupAudioData>) -> 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<T : AudioGroup>(protected val soundType: String) :
TransformSerializer<T, List<RawSubGroupAudioData>>(kotlinx.serialization.serializer()) {
/**
* Creates an instance of [T] from a map of subtype identifiers to [SubGroupAudioData].
*/
protected abstract fun toAudioGroup(map: Map<String, SubGroupAudioData>): T
/**
* Registers the conversion function for this [AudioGroup] subclass.
*/
public fun register() {
registerConversion(soundType, ::toAudioGroup)
}
override fun toData(value: T): List<RawSubGroupAudioData> {
return value.map { (subType, data) ->
data.toRaw(subType)
}
}
override fun fromData(data: List<RawSubGroupAudioData>): T {
val map = data.associateBy { it.groupSubtype }
return toAudioGroup(map)
}
}
public object Serializer : BaseSerializer<AudioGroup>("Custom") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): 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<String, SubGroupAudioData> = mutableMapOf(),
@Transient
override var soundType: String = "Custom",
) : AudioGroup(subTypes), MutableMap<String, SubGroupAudioData> by subTypes {
public object Serializer :
PolymorphicDelegatedSerializer<CustomAudioGroup, AudioGroup>(kotlinx.serialization.serializer())
}
@@ -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<String, SubGroupAudioData> = 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<BigMistakeAudioGroup>("BigMistake") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): BigMistakeAudioGroup {
return BigMistakeAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<BurnshotSoundAudioGroup>("BurnshotSound") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): BurnshotSoundAudioGroup {
return BurnshotSoundAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<ClapSoundHoldAudioGroup>("ClapSoundHold") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): ClapSoundHoldAudioGroup {
return ClapSoundHoldAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<ClapSoundHoldP2AudioGroup>("ClapSoundHoldP2") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): ClapSoundHoldP2AudioGroup {
return ClapSoundHoldP2AudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<FreezeshotSoundAudioGroup>("FreezeshotSound") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): FreezeshotSoundAudioGroup {
return FreezeshotSoundAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<Hand1PopSoundAudioGroup>("Hand1PopSound") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): Hand1PopSoundAudioGroup {
return Hand1PopSoundAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<Hand2PopSoundAudioGroup>("Hand2PopSound") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): Hand2PopSoundAudioGroup {
return Hand2PopSoundAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<HeartExplosion2AudioGroup>("HeartExplosion2") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): HeartExplosion2AudioGroup {
return HeartExplosion2AudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<HeartExplosion3AudioGroup>("HeartExplosion3") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): HeartExplosion3AudioGroup {
return HeartExplosion3AudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<HeartExplosionAudioGroup>("HeartExplosion") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): HeartExplosionAudioGroup {
return HeartExplosionAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<PulseSoundHoldAudioGroup>("PulseSoundHold") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): PulseSoundHoldAudioGroup {
return PulseSoundHoldAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<PulseSoundHoldP2AudioGroup>("PulseSoundHoldP2") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): PulseSoundHoldP2AudioGroup {
return PulseSoundHoldP2AudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<SkipshotAudioGroup>("Skipshot") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): SkipshotAudioGroup {
return SkipshotAudioGroup(map)
}
init {
register()
}
}
}
@@ -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<String, SubGroupAudioData> = 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<SmallMistakeAudioGroup>("SmallMistake") {
override fun toAudioGroup(map: Map<String, SubGroupAudioData>): SmallMistakeAudioGroup {
return SmallMistakeAudioGroup(map)
}
init {
register()
}
}
}
@@ -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.
@@ -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) {
@@ -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." }
}
}
}
@@ -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()
@@ -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,
}
}
@@ -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
}
}
@@ -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
)
}
@@ -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,
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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) },
)
}
@@ -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<AudioData>? = 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
}
}
}
}
@@ -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
}
}
@@ -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<SetGameSoundEvent>(DataSerializer) {
@OptIn(RDKTInternalAPI::class)
private object DataSerializer :
TransformSerializer<SetGameSoundEvent, SetGameSoundEvent>(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)
}
}
}
@@ -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
}
}
@@ -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
}
}
@@ -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<Sub : Base, Base>(
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!!
}
}
@@ -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
@@ -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<T, Data>(
private val dataSerializer: KSerializer<Data>
) : KSerializer<T> {
@@ -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
}
}
@@ -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)
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
[versions]
agp = "8.5.2"
kotlin = "2.1.10"
kotlin = "2.3.0"
android-minSdk = "24"
android-compileSdk = "34"