diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index a65ed9525..010304a53 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,13 +1,13 @@ - - - \ No newline at end of file diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/EntryPoint.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/EntryPoint.kt index 3f79a4614..a90f88051 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/EntryPoint.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/EntryPoint.kt @@ -1,17 +1,16 @@ package net.torvald.terrarum.musicplayer -import net.torvald.terrarum.IngameInstance import net.torvald.terrarum.ModMgr import net.torvald.terrarum.ModuleEntryPoint import net.torvald.terrarum.modulebasegame.TerrarumIngame -import net.torvald.terrarum.musicplayer.gui.MusicPlayer +import net.torvald.terrarum.musicplayer.gui.MusicPlayerControl /** * Created by minjaesong on 2023-12-23. */ class EntryPoint : ModuleEntryPoint() { override fun invoke() { - ModMgr.GameExtraGuiLoader.register { ingame: TerrarumIngame -> MusicPlayer(ingame) } + ModMgr.GameExtraGuiLoader.register { ingame: TerrarumIngame -> MusicPlayerControl(ingame) } } override fun dispose() { diff --git a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt similarity index 91% rename from MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt rename to MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt index f754de1f1..52b0375a4 100644 --- a/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayer.kt +++ b/MusicPlayer/src/net/torvald/terrarum/musicplayer/gui/MusicPlayerControl.kt @@ -10,7 +10,6 @@ import com.badlogic.gdx.utils.JsonValue import com.jme3.math.FastMath import net.torvald.reflection.extortField import net.torvald.terrarum.* -import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.audio.* import net.torvald.terrarum.audio.audiobank.MusicContainer import net.torvald.terrarum.gameworld.fmod @@ -33,7 +32,7 @@ import kotlin.math.* * * Created by minjaesong on 2023-12-23. */ -class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { +class MusicPlayerControl(private val ingame: TerrarumIngame) : UICanvas() { private val STRIP_W = 9f private val METERS_WIDTH = 2 * STRIP_W @@ -96,20 +95,21 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { /** Returns the internal playlist of the MusicGovernor */ private val songsInGovernor: List - get() = ingame.musicGovernor.extortField>("songs")!! + get() = ingame.backgroundMusicPlayer.extortField>("songs")!! - private val shouldPlayerBeDisabled: Boolean + /*private val shouldPlayerBeDisabled: Boolean get() { - return App.audioMixer.dynamicTracks.any { it.isPlaying && it.trackingTarget is PlaysMusic } - } + return MusicService.transactionLocked + //return App.audioMixer.dynamicTracks.any { it.isPlaying && it.trackingTarget is PlaysMusic } + }*/ /** Returns the playlist name from the MusicGovernor. Getting the value from the MusicGovernor * is recommended as an ingame interaction may cancel the playback from the playlist from the MusicPlayer * (e.g. interacting with a jukebox) */ private val internalPlaylistName: String - get() = ingame.musicGovernor.playlistName + get() = ingame.backgroundMusicPlayer.playlistName - fun registerPlaylist(path: String, fileToName: JsonValue?, shuffled: Boolean, diskJockeyingMode: String) { + fun registerPlaylist(path: String, fileToName: JsonValue?, shuffled: Boolean, diskJockeyingMode: String): TerrarumMusicPlaylist { fun String.isNum(): Boolean { try { this.toInt() @@ -120,7 +120,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { } } - ingame.musicGovernor.queueDirectory(path, shuffled, diskJockeyingMode) { filename -> + val playlist = ingame.backgroundMusicPlayer.queueDirectory(path, shuffled, diskJockeyingMode, false) { filename -> fileToName?.get(filename).let { if (it == null) filename.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.let { @@ -136,19 +136,21 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { } } - ingame.musicGovernor.addMusicStartHook { music -> + ingame.backgroundMusicPlayer.addMusicStartHook { music -> setMusicName(music.name) if (mode <= MODE_PLAYING) transitionRequest = MODE_PLAYING } - ingame.musicGovernor.addMusicStopHook { music -> + ingame.backgroundMusicPlayer.addMusicStopHook { music -> setIntermission() if (mode <= MODE_PLAYING) transitionRequest = MODE_IDLE } setPlaylistDisplayVars(songsInGovernor) + + return playlist } private var currentMusicName = "" @@ -186,7 +188,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { private var mouseOnList: Int? = null override fun updateImpl(delta: Float) { - val shouldPlayerBeDisabled = shouldPlayerBeDisabled + val transactionLocked = MusicService.transactionLocked // process transition request if (transitionRequest != null) { @@ -345,16 +347,10 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { 1 -> { // prev // prev song if (mode < MODE_SHOW_LIST) { - getPrevSongFromPlaylist()?.let { ingame.musicGovernor.unshiftPlaylist(it) } - if (!shouldPlayerBeDisabled) { - App.audioMixer.requestFadeOut( - App.audioMixer.musicTrack, - AudioMixer.DEFAULT_FADEOUT_LEN / 3f - ) { - ingame.musicGovernor.startMusic(this) // required for "intermittent" mode - iHitTheStopButton = false - stopRequested = false - } + MusicService.playPrevSongInPlaylist { + ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode + iHitTheStopButton = false + stopRequested = false } } // prev page in the playlist @@ -372,18 +368,31 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { 2 -> { // stop if (mode < MODE_SHOW_LIST) { // disable stop button entirely on MODE_SHOW_LIST + // when the button is STOP if (App.audioMixer.musicTrack.isPlaying) { - val thisMusic = App.audioMixer.musicTrack.currentTrack + // FIXME the olde way -- must be replaced with one that utilises MusicService + /*val thisMusic = App.audioMixer.musicTrack.currentTrack App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) App.audioMixer.musicTrack.nextTrack = null - ingame.musicGovernor.stopMusic(this) - if (thisMusic is MusicContainer) thisMusic.let { ingame.musicGovernor.queueMusicToPlayNext(it) } - iHitTheStopButton = true + ingame.backgroundMusicPlayer.stopMusic(this) + if (thisMusic is MusicContainer) thisMusic.let { ingame.backgroundMusicPlayer.queueMusicToPlayNext(it) } + iHitTheStopButton = true*/ + + MusicService.stopPlaylistPlayback { + iHitTheStopButton = true + } } - else if (!shouldPlayerBeDisabled) { - ingame.musicGovernor.startMusic(this) + // when the button is PLAY + else if (!App.audioMixer.musicTrack.isPlaying) { + // FIXME the olde way -- must be replaced with one that utilises MusicService + /*ingame.backgroundMusicPlayer.startMusic(this) iHitTheStopButton = false - stopRequested = false + stopRequested = false*/ + + MusicService.resumePlaylistPlayback { + iHitTheStopButton = false + stopRequested = false + } } } } @@ -391,15 +400,10 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { 3 -> { // next // next song if (mode < MODE_SHOW_LIST) { - if (!shouldPlayerBeDisabled) { - App.audioMixer.requestFadeOut( - App.audioMixer.musicTrack, - AudioMixer.DEFAULT_FADEOUT_LEN / 3f - ) { - ingame.musicGovernor.startMusic(this) // required for "intermittent" mode, does seemingly nothing on "continuous" mode - iHitTheStopButton = false - stopRequested = false - } + MusicService.playNextSongInPlaylist { + ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode + iHitTheStopButton = false + stopRequested = false } } // next page in the playlist @@ -435,28 +439,34 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { } } } - // make playlist clicking work + // make playlist clicking (change song within the playlist) work else if (listViewPanelScroll == 1f && mouseOnList != null) { val index = playlistScroll + mouseOnList!! val list = songsInGovernor if (index < list.size) { // if selected song != currently playing if (App.audioMixer.musicTrack.currentTrack == null || list[index] != App.audioMixer.musicTrack.currentTrack) { + // FIXME the olde way -- must be replaced with one that utilises MusicService // rebuild playlist - ingame.musicGovernor.queueIndexFromPlaylist(index) - + //ingame.backgroundMusicPlayer.queueIndexFromPlaylist(index) // fade out - App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { + /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { if (!shouldPlayerBeDisabled) { - ingame.musicGovernor.startMusic(this) // required for "intermittent" mode + ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode iHitTheStopButton = false stopRequested = false } + }*/ + + MusicService.playNthSongInPlaylist(index) { + ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode + iHitTheStopButton = false + stopRequested = false } } } } - // make album list clicking work + // make album list clicking (send new playlist to the MusicService) work else if (listViewPanelScroll == 0f && mouseOnList != null) { val index = albumlistScroll + mouseOnList!! val list = albumsList//.map { albumPropCache[it] } @@ -464,16 +474,23 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { if (index < list.size) { // if selected album is not the same album currently playing, queue that album immediately // (navigating into the selected album involves too much complication :p) - if (ingame.musicGovernor.playlistSource != albumsList[index].canonicalPath) { + if (ingame.backgroundMusicPlayer.playlistSource != albumsList[index].canonicalPath) { + // FIXME the olde way -- must be replaced with one that utilises MusicService // fade out - App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { + /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 3f) { loadNewAlbum(albumsList[index]) if (!shouldPlayerBeDisabled) { - ingame.musicGovernor.startMusic(this) // required for "intermittent" mode + ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode iHitTheStopButton = false stopRequested = false } resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) + }*/ + + val playlist = loadNewAlbum(albumsList[index]) + MusicService.putNewPlaylist(playlist) { + resetPlaylistScroll(App.audioMixer.musicTrack.nextTrack as? MusicContainer) + App.audioMixer.startMusic(playlist.getCurrent()) } } } @@ -487,16 +504,16 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { // printdbg(this, "mode = $mode; req = $transitionRequest") - if (shouldPlayerBeDisabled || iHitTheStopButton) { + /*if (shouldPlayerBeDisabled || iHitTheStopButton) { if (!stopRequested) { stopRequested = true - ingame.musicGovernor.stopMusic(this) + ingame.backgroundMusicPlayer.stopMusic(this) } } - else if (ingame.musicGovernor.playCaller is PlaysMusic && !jukeboxStopMonitorAlert && !App.audioMixer.musicTrack.isPlaying) { + else*/ if (ingame.backgroundMusicPlayer.playCaller is PlaysMusic && !jukeboxStopMonitorAlert && !App.audioMixer.musicTrack.isPlaying) { jukeboxStopMonitorAlert = true - val interval = ingame.musicGovernor.getRandomMusicInterval() - ingame.musicGovernor.stopMusic(this, false, interval) + val interval = ingame.backgroundMusicPlayer.getRandomMusicInterval() + ingame.backgroundMusicPlayer.stopMusic(this, false, interval) } else if (App.audioMixer.musicTrack.isPlaying) { jukeboxStopMonitorAlert = false @@ -508,7 +525,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { private var stopRequested = false private fun resetAlbumlistScroll() { - val currentlyPlaying = albumsList.indexOfFirst { it.canonicalPath.replace('\\', '/') == ingame.musicGovernor.playlistSource } + val currentlyPlaying = albumsList.indexOfFirst { it.canonicalPath.replace('\\', '/') == ingame.backgroundMusicPlayer.playlistSource } if (currentlyPlaying >= 0) { albumlistScroll = (currentlyPlaying / PLAYLIST_LINES) * PLAYLIST_LINES } @@ -527,7 +544,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { } } - private fun getPrevSongFromPlaylist(): MusicContainer? { + /*private fun getPrevSongFromPlaylist(): MusicContainer? { val list = songsInGovernor.slice(songsInGovernor.indices) // make copy of the list val nowPlaying = App.audioMixer.musicTrack.currentTrack ?: return null @@ -537,7 +554,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { val prevIndex = (currentIndex - 1).fmod(list.size) return list[prevIndex] - } + }*/ // private fun smoothstep(x: Float) = (x*x*(3f-2f*x)).coerceIn(0f, 1f) // private fun smootherstep(x: Float) = (x*x*x*(x*(6f*x-15f)+10f)).coerceIn(0f, 1f) @@ -919,7 +936,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { val pnum = i + albumlistScroll val currentlyPlaying = if (pnum in albumsList.indices) { - val m1 = ingame.musicGovernor.playlistSource + val m1 = ingame.backgroundMusicPlayer.playlistSource val m2 = albumsList[pnum].canonicalPath.replace('\\', '/') (m1 == m2) } @@ -1279,14 +1296,14 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { val albumArt: TextureRegion? = null ) - private fun loadNewAlbum(albumDir: File) { + private fun loadNewAlbum(albumDir: File): TerrarumMusicPlaylist { val albumProp = albumPropCache[albumDir] App.audioMixer.musicTrack.let { track -> track.doGaplessPlayback = (albumProp.diskJockeyingMode == "continuous") if (track.doGaplessPlayback) { track.pullNextTrack = { - track.currentTrack = ingame.musicGovernor.pullNextMusicTrack(true) + track.currentTrack = ingame.backgroundMusicPlayer.pullNextMusicTrack(true) setMusicName(track.currentTrack?.name ?: "") } } @@ -1294,7 +1311,7 @@ class MusicPlayer(private val ingame: TerrarumIngame) : UICanvas() { currentlySelectedAlbum = albumProp - registerPlaylist(albumDir.absolutePath, albumProp.fileToName, albumProp.shuffled, albumProp.diskJockeyingMode) + return registerPlaylist(albumDir.absolutePath, albumProp.fileToName, albumProp.shuffled, albumProp.diskJockeyingMode) // scroll playlist to the page current song is } diff --git a/src/net/torvald/terrarum/MusicGovernor.kt b/src/net/torvald/terrarum/BackgroundMusicPlayer.kt similarity index 90% rename from src/net/torvald/terrarum/MusicGovernor.kt rename to src/net/torvald/terrarum/BackgroundMusicPlayer.kt index 78f30cc7b..963b64c70 100644 --- a/src/net/torvald/terrarum/MusicGovernor.kt +++ b/src/net/torvald/terrarum/BackgroundMusicPlayer.kt @@ -1,6 +1,6 @@ package net.torvald.terrarum -open class MusicGovernor { +open class BackgroundMusicPlayer { open fun update(ingameInstance: IngameInstance, delta: Float) { diff --git a/src/net/torvald/terrarum/IngameInstance.kt b/src/net/torvald/terrarum/IngameInstance.kt index c74a1ee3f..238be60e4 100644 --- a/src/net/torvald/terrarum/IngameInstance.kt +++ b/src/net/torvald/terrarum/IngameInstance.kt @@ -1,12 +1,8 @@ package net.torvald.terrarum -import com.badlogic.gdx.Gdx import com.badlogic.gdx.Input -import com.badlogic.gdx.utils.Disposable import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE -import net.torvald.terrarum.audio.audiobank.MusicContainer -import net.torvald.terrarum.audio.dsp.BinoPan import net.torvald.terrarum.gameactors.Actor import net.torvald.terrarum.gameactors.ActorID import net.torvald.terrarum.gameactors.ActorWithBody @@ -37,7 +33,6 @@ import java.nio.file.Files import java.nio.file.StandardCopyOption import java.util.* import java.util.concurrent.locks.Lock -import java.util.function.Consumer import kotlin.math.min /** @@ -586,7 +581,7 @@ open class IngameInstance(val batch: FlippingSpriteBatch, val isMultiplayer: Boo noticelet.sendNotification(itemID, itemCount) } - open val musicGovernor: MusicGovernor = MusicGovernor() + open val backgroundMusicPlayer: BackgroundMusicPlayer = BackgroundMusicPlayer() } inline fun Lock.lock(body: () -> Unit) { diff --git a/src/net/torvald/terrarum/MusicService.kt b/src/net/torvald/terrarum/MusicService.kt new file mode 100644 index 000000000..95542d739 --- /dev/null +++ b/src/net/torvald/terrarum/MusicService.kt @@ -0,0 +1,259 @@ +package net.torvald.terrarum + +import net.torvald.terrarum.audio.AudioMixer.Companion.DEFAULT_FADEOUT_LEN +import net.torvald.terrarum.transaction.Transaction +import net.torvald.terrarum.transaction.TransactionListener +import net.torvald.terrarum.transaction.TransactionState + +/** + * To play the music, create a transaction then pass it to the `runTransaction(Transaction)` + * + * Created by minjaesong on 2024-06-28. + */ +object MusicService : TransactionListener() { + + private var currentPlaylist: TerrarumMusicPlaylist? = null + + override fun getCurrentStatusForTransaction(): TransactionState { + return TransactionState( + mutableMapOf( + "currentPlaylist" to currentPlaylist + ) + ) + } + + override fun commitTransaction(state: TransactionState) { + this.currentPlaylist = state["currentPlaylist"] as TerrarumMusicPlaylist? + } + + /** + * Puts the given playlist to this object if the transaction successes. If the given playlist is same as the + * current playlist, the transaction will successfully finish immediately; otherwise the given playlist will + * be reset as soon as the transaction starts. Note that the resetting behaviour is NOT atomic. (the given + * playlist will stay in reset state even if the transaction fails) + * + * The old playlist will be disposed of if and only if the transaction was successful. + * + * @param playlist An instance of a [TerrarumMusicPlaylist] to be changed into + * @param onSuccess What to do after the transaction. Default behaviour is: `App.audioMixer.startMusic(playlist.getCurrent())` + */ + private fun createTransactionPlaylistChange(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit = { + App.audioMixer.startMusic(playlist.getCurrent()) + }): Transaction { + return object : Transaction { + var oldPlaylist: TerrarumMusicPlaylist? = null + + override fun start(state: TransactionState) { + oldPlaylist = state["currentPlaylist"] as TerrarumMusicPlaylist? + + if (oldPlaylist == playlist) return + + playlist.reset() + + // request fadeout + if (App.audioMixer.musicTrack.isPlaying) { + var fadedOut = false + + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { + // put new playlist + state["currentPlaylist"] = playlist + + fadedOut = true + } + + waitUntil { fadedOut } + } + else { + /* do nothing */ + } + } + + override fun onSuccess(state: TransactionState) { + oldPlaylist?.dispose() + onSuccess() + } + override fun onFailure(e: Throwable, state: TransactionState) {} + } + } + + private fun createTransactionForNextMusicInPlaylist(onSuccess: () -> Unit): Transaction { + return object : Transaction { + override fun start(state: TransactionState) { + var fadedOut = false + // request fadeout + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { + // callback: play next song in the playlist + App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getNext()) + fadedOut = true + } + + waitUntil { fadedOut } + } + + override fun onSuccess(state: TransactionState) { + onSuccess() + } + override fun onFailure(e: Throwable, state: TransactionState) {} + } + } + + private fun createTransactionForPrevMusicInPlaylist(onSuccess: () -> Unit): Transaction { + return object : Transaction { + override fun start(state: TransactionState) { + var fadedOut = false + // request fadeout + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { + // callback: play prev song in the playlist + App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getPrev()) + fadedOut = true + } + + waitUntil { fadedOut } + } + + override fun onSuccess(state: TransactionState) { + onSuccess() + } + override fun onFailure(e: Throwable, state: TransactionState) {} + } + } + + private fun createTransactionForNthMusicInPlaylist(index: Int, onSuccess: () -> Unit): Transaction { + return object : Transaction { + override fun start(state: TransactionState) { + var fadedOut = false + // request fadeout + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { + // callback: play prev song in the playlist + App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getNthSong(index)) + fadedOut = true + } + + waitUntil { fadedOut } + } + + override fun onSuccess(state: TransactionState) { onSuccess() } + override fun onFailure(e: Throwable, state: TransactionState) {} + } + } + + private fun createTransactionForPlaylistStop(onSuccess: () -> Unit): Transaction { + return object : Transaction { + override fun start(state: TransactionState) { + var fadedOut = false + // request fadeout + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack) { + fadedOut = true + } + + waitUntil { fadedOut } + } + + override fun onSuccess(state: TransactionState) { onSuccess() } + override fun onFailure(e: Throwable, state: TransactionState) {} + } + } + + private fun createTransactionForPlaylistResume(onSuccess: () -> Unit): Transaction { + return object : Transaction { + override fun start(state: TransactionState) { + App.audioMixer.startMusic((state["currentPlaylist"] as TerrarumMusicPlaylist).getCurrent()) + } + + override fun onSuccess(state: TransactionState) { onSuccess() } + override fun onFailure(e: Throwable, state: TransactionState) {} + } + } + + private fun createTransactionPausePlaylistForMusicalFixture( + action: () -> Unit, + musicFinished: () -> Boolean, + onSuccess: () -> Unit, + onFailure: (Throwable) -> Unit + ): Transaction { + return object : Transaction { + override fun start(state: TransactionState) { + var fadedOut = false + // request fadeout + App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, DEFAULT_FADEOUT_LEN / 2.0) { + // callback: let the caller actually take care of playing the audio + action() + + fadedOut = true + } + + waitUntil { fadedOut } + + // wait until the interjected music finishes + waitUntil { musicFinished() } + } + + override fun onSuccess(state: TransactionState) { onSuccess() } + override fun onFailure(e: Throwable, state: TransactionState) { onFailure(e) } + } + + // note to self: wait() and notify() using a lock object is impractical as the Java thread can wake up + // randomly regardless of the notify(), which results in the common pattern of + // while (!condition) { lock.wait() } + // and if we need extra condition (i.e. musicFinished()), it's just a needlessly elaborate way of spinning, + // UNLESS THE THING MUST BE SYNCHRONISED WITH SOMETHING + } + + private fun waitUntil(escapeCondition: () -> Boolean) { + while (!escapeCondition()) { + Thread.sleep(4L) + } + } + + fun putNewPlaylist(playlist: TerrarumMusicPlaylist, onSuccess: (() -> Unit)? = null) { + if (onSuccess != null) + runTransaction(createTransactionPlaylistChange(playlist, onSuccess)) + else + runTransaction(createTransactionPlaylistChange(playlist)) + } + fun putNewPlaylist(playlist: TerrarumMusicPlaylist, onSuccess: () -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionPlaylistChange(playlist, onSuccess), onFinally) + } + + fun playMusicalFixture(action: () -> Unit, musicFinished: () -> Boolean, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit) { + runTransaction(createTransactionPausePlaylistForMusicalFixture(action, musicFinished, onSuccess, onFailure)) + } + fun playMusicalFixture(action: () -> Unit, musicFinished: () -> Boolean, onSuccess: () -> Unit, onFailure: (Throwable) -> Unit, onFinally: () -> Unit = {}) { + runTransaction(createTransactionPausePlaylistForMusicalFixture(action, musicFinished, onSuccess, onFailure), onFinally) + } + + fun playNextSongInPlaylist(onSuccess: () -> Unit) { + runTransaction(createTransactionForNextMusicInPlaylist(onSuccess)) + } + fun playNextSongInPlaylist(onSuccess: () -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionForNextMusicInPlaylist(onSuccess), onFinally) + } + + fun playPrevSongInPlaylist(onSuccess: () -> Unit) { + runTransaction(createTransactionForPrevMusicInPlaylist(onSuccess)) + } + fun playPrevSongInPlaylist(onSuccess: () -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionForPrevMusicInPlaylist(onSuccess), onFinally) + } + + fun playNthSongInPlaylist(index: Int, onSuccess: () -> Unit) { + runTransaction(createTransactionForNthMusicInPlaylist(index, onSuccess)) + } + fun playNthSongInPlaylist(index: Int, onSuccess: () -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionForNthMusicInPlaylist(index, onSuccess), onFinally) + } + + fun stopPlaylistPlayback(onSuccess: () -> Unit) { + runTransaction(createTransactionForPlaylistStop(onSuccess)) + } + fun stopPlaylistPlayback(onSuccess: () -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionForPlaylistStop(onSuccess), onFinally) + } + + fun resumePlaylistPlayback(onSuccess: () -> Unit) { + runTransaction(createTransactionForPlaylistResume(onSuccess)) + } + fun resumePlaylistPlayback(onSuccess: () -> Unit, onFinally: () -> Unit) { + runTransaction(createTransactionForPlaylistResume(onSuccess), onFinally) + } +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt b/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt new file mode 100644 index 000000000..4d1b9547d --- /dev/null +++ b/src/net/torvald/terrarum/TerrarumMusicPlaylist.kt @@ -0,0 +1,142 @@ +package net.torvald.terrarum + +import com.badlogic.gdx.utils.Disposable +import com.badlogic.gdx.utils.GdxRuntimeException +import net.torvald.terrarum.App.printdbg +import net.torvald.terrarum.audio.audiobank.MusicContainer +import java.io.File + +/** + * The `musicList` never (read: should not) gets changed, only the `internalIndices` are being changed as + * the songs are being played. + * + * Created by minjaesong on 2024-06-29. + */ +class TerrarumMusicPlaylist( + /** list of files */ + val musicList: List, + /** name of the album/playlist shown in the [net.torvald.terrarum.musicplayer.gui.MusicPlayer] */ + val name: String, + /** canonicalPath with path separators converted to forward slash */ + val source: String, + /** "continuous", "intermittent"; not used by the Playlist itself but by the BackgroundMusicPlayer (aka you are the one who make it actually work) */ + val diskJockeyingMode: String, + /** if set, the `internalIndices` will be shuffled accordingly, and this happens automatically. (aka you don't need to worry about) */ + val shuffled: Boolean +): Disposable { + + private val internalIndices = ArrayList() + private var currentIndexCursor = musicList.size + + init { + reset() + } + + fun reset() { + internalIndices.clear() + refillInternalIndices() + refillInternalIndices() + currentIndexCursor = musicList.size + } + + private fun checkRefill() { + if (internalIndices.size < currentIndexCursor + 1) + refillInternalIndices() + } + + fun getCurrent(): MusicContainer { + checkRefill() + + return musicList[currentIndexCursor] + } + + fun getNext(): MusicContainer { + checkRefill() + currentIndexCursor += 1 + + return musicList[currentIndexCursor] + } + + fun getPrev(): MusicContainer { + if (currentIndexCursor == 0) { + if (shuffled) { + musicList.indices.toMutableList().also { if (shuffled) it.shuffle() }.reversed().forEach { + internalIndices.add(0, it) + } + currentIndexCursor += musicList.size + } + else { + musicList.indices.reversed().forEach { + internalIndices.add(0, it) + } + currentIndexCursor += musicList.size + } + } + + currentIndexCursor -= 1 + + return musicList[currentIndexCursor] + } + + + private fun refillInternalIndices() { + internalIndices.addAll(musicList.indices.toMutableList().also { if (shuffled) it.shuffle() }) + } + + inline fun getNthSong(n: Int) = musicList[n] + + override fun dispose() { + musicList.forEach { + it.tryDispose() + } + } + + companion object { + /** + * Adding songFinishedHook to the songs is a responsibility of the caller. + */ + fun fromDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, fileToName: ((String) -> String) = { name: String -> + name.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.joinToString(" ") + }): TerrarumMusicPlaylist { + val musicDir = musicDir.replace('\\', '/') + val playlistSource = musicDir + printdbg(this, "registerSongsFromDir $musicDir") + + val playlistName = musicDir.substringAfterLast('/') + + val playlist = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull { + printdbg(this, "Music: ${it.absolutePath}") + try { + MusicContainer( + fileToName(it.name), + it + )/*.also { muscon -> + + printdbg(this, "MusicTitle: ${muscon.name}") + + muscon.songFinishedHook = { + if (App.audioMixer.musicTrack.currentTrack == it) { + stopMusic(this, true, getRandomMusicInterval()) + } + } + }*/ + // adding songFinishedHook must be done by the caller + } + catch (e: GdxRuntimeException) { + e.printStackTrace() + null + } + } ?: emptyList() // TODO test code + + return TerrarumMusicPlaylist( + playlist, + playlistName, + musicDir, + diskJockeyingMode, + shuffled + ) + } + } + + +} \ No newline at end of file diff --git a/src/net/torvald/terrarum/audio/AudioMixer.kt b/src/net/torvald/terrarum/audio/AudioMixer.kt index a88651973..ba7f92ff9 100644 --- a/src/net/torvald/terrarum/audio/AudioMixer.kt +++ b/src/net/torvald/terrarum/audio/AudioMixer.kt @@ -529,7 +529,11 @@ class AudioMixer : Disposable { // fade will be processed by the update() } - fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double = DEFAULT_FADEOUT_LEN, target: Double = 0.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { + /** + * Preferably, audio apps should NOT call this function directly to change music, [MusicService] must be used + * to control the music playback instead. + */ + internal fun requestFadeOut(track: TerrarumAudioMixerTrack, length: Double = DEFAULT_FADEOUT_LEN, target: Double = 0.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { val req = fadeReqs[track]!! if (!req.fadeoutFired) { req.fadeLength = length.coerceAtLeast(1.0/1024.0) @@ -541,7 +545,11 @@ class AudioMixer : Disposable { } } - fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { + /** + * Preferably, audio apps should NOT call this function directly to change music, [MusicService] must be used + * to control the music playback instead. + */ + internal fun requestFadeIn(track: TerrarumAudioMixerTrack, length: Double, target: Double = 1.0, source: Double? = null, jobAfterFadeout: () -> Unit = {}) { // printdbg(this, "fadein called by") // printStackTrace(this) diff --git a/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt b/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt index d31d0a7ca..865a34540 100644 --- a/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt +++ b/src/net/torvald/terrarum/modulebasegame/BuildingMaker.kt @@ -62,7 +62,7 @@ class BuildingMaker(batch: FlippingSpriteBatch) : IngameInstance(batch) { lateinit var gameWorld: GameWorld - override val musicGovernor = TerrarumMusicGovernor() + override val backgroundMusicPlayer = TerrarumBackgroundMusicPlayer() init { gameUpdateGovernor = ConsistentUpdateRate @@ -396,7 +396,7 @@ class BuildingMaker(batch: FlippingSpriteBatch) : IngameInstance(batch) { } - musicGovernor.update(this, delta) + backgroundMusicPlayer.update(this, delta) } @@ -495,7 +495,7 @@ class BuildingMaker(batch: FlippingSpriteBatch) : IngameInstance(batch) { // blockMarkings.dispose() uiPenMenu.dispose() uiGetPoiName.dispose() - musicGovernor.dispose() + backgroundMusicPlayer.dispose() } fun getPoiNameForExport(w: Int, h: Int, callback: (String) -> Unit) { diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumBackgroundMusicPlayer.kt similarity index 87% rename from src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt rename to src/net/torvald/terrarum/modulebasegame/TerrarumBackgroundMusicPlayer.kt index e0c59b2cd..d54d97984 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumMusicGovernor.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumBackgroundMusicPlayer.kt @@ -11,7 +11,7 @@ import net.torvald.terrarum.audio.audiobank.MusicContainer import net.torvald.terrarum.gameworld.WorldTime.Companion.DAY_LENGTH import java.io.File -class TerrarumMusicGovernor : MusicGovernor() { +class TerrarumBackgroundMusicPlayer : BackgroundMusicPlayer() { private val STATE_INIT = 0 private val STATE_FIREPLAY = 1 private val STATE_PLAYING = 2 @@ -22,7 +22,7 @@ class TerrarumMusicGovernor : MusicGovernor() { musicState = STATE_INTERMISSION } - private var songs: List = emptyList() + private var playlist: List = emptyList() var playlistName = ""; private set /** canonicalPath with path separators converted to forward slash */ var playlistSource = "" ; private set @@ -42,7 +42,7 @@ class TerrarumMusicGovernor : MusicGovernor() { playlistName = musicDir.substringAfterLast('/') - songs = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull { + playlist = File(musicDir).listFiles()?.sortedBy { it.name }?.mapNotNull { printdbg(this, "Music: ${it.absolutePath}") try { MusicContainer( @@ -67,77 +67,91 @@ class TerrarumMusicGovernor : MusicGovernor() { } private fun restockMusicBin() { - musicBin = ArrayList(if (shuffled) songs.shuffled() else songs.slice(songs.indices)) + musicBin = ArrayList(if (shuffled) playlist.shuffled() else playlist.slice(playlist.indices)) } /** + * Send the playlist to the MusicService to be played at the other play commands. + * * @param musicDir where the music files are. Absolute path. * @param shuffled if the tracks are to be shuffled * @param diskJockeyingMode `intermittent` to give random gap between tracks, `continuous` for continuous playback */ - fun queueDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, fileToName: ((String) -> String)? = null) { - if (musicState != STATE_INIT && musicState != STATE_INTERMISSION) { + fun queueDirectory(musicDir: String, shuffled: Boolean, diskJockeyingMode: String, playImmediately: Boolean, fileToName: ((String) -> String) = { name: String -> + name.substringBeforeLast('.').replace('_', ' ').split(" ").map { it.capitalize() }.joinToString(" ") + }): TerrarumMusicPlaylist { + // FIXME the olde way -- must be replaced with one that utilises MusicService + /*if (musicState != STATE_INIT && musicState != STATE_INTERMISSION) { App.audioMixer.requestFadeOut(App.audioMixer.fadeBus, AudioMixer.DEFAULT_FADEOUT_LEN) // explicit call for fade-out when the game instance quits stopMusic0(App.audioMixer.musicTrack.currentTrack) } - songs.forEach { it.tryDispose() } + playlist.forEach { it.tryDispose() } registerSongsFromDir(musicDir, fileToName) this.shuffled = shuffled this.diskJockeyingMode = diskJockeyingMode - restockMusicBin() + restockMusicBin()*/ + + val playlist = TerrarumMusicPlaylist.fromDirectory(musicDir, shuffled, diskJockeyingMode, fileToName) + + if (!playImmediately) + MusicService.putNewPlaylist(playlist) {} + else + MusicService.putNewPlaylist(playlist) + + return playlist } /** * Adds a song to the head of the internal playlist (`musicBin`) */ - fun queueMusicToPlayNext(music: MusicContainer) { + fun xxxqueueMusicToPlayNext(music: MusicContainer) { musicBin.add(0, music) } /** * Unshifts an internal playlist (`musicBin`). The `music` argument must be the song that exists on the `songs`. */ - fun unshiftPlaylist(music: MusicContainer) { - val indexAtMusicBin = songs.indexOf(music) + fun xxxunshiftPlaylist(music: MusicContainer) { + val indexAtMusicBin = playlist.indexOf(music) if (indexAtMusicBin < 0) throw IllegalArgumentException("The music does not exist on the internal songs list ($music)") // rewrite musicBin - val newMusicBin = if (shuffled) songs.shuffled().toTypedArray().also { + val newMusicBin = if (shuffled) playlist.shuffled().toTypedArray().also { // if shuffled, // 1. create a shuffled version of songlist // 2. swap two songs such that the songs[indexAtMusicBin] comes first - val swapTo = it.indexOf(songs[indexAtMusicBin]) + val swapTo = it.indexOf(playlist[indexAtMusicBin]) val tmp = it[swapTo] it[swapTo] = it[0] it[0] = tmp } - else Array(songs.size - indexAtMusicBin) { offset -> + else Array(playlist.size - indexAtMusicBin) { offset -> val k = offset + indexAtMusicBin - songs[k] + playlist[k] } musicBin = ArrayList(newMusicBin.toList()) } - fun queueIndexFromPlaylist(indexAtMusicBin: Int) { - if (indexAtMusicBin !in songs.indices) throw IndexOutOfBoundsException("The index is outside of the internal songs list ($indexAtMusicBin/${songs.size})") + fun xxxqueueIndexFromPlaylist(indexAtMusicBin: Int) { + if (indexAtMusicBin !in playlist.indices) throw IndexOutOfBoundsException("The index is outside of the internal songs list ($indexAtMusicBin/${playlist.size})") // rewrite musicBin - val newMusicBin = if (shuffled) songs.shuffled().toTypedArray().also { + val newMusicBin = if (shuffled) playlist.shuffled().toTypedArray().also { // if shuffled, // 1. create a shuffled version of songlist // 2. swap two songs such that the songs[indexAtMusicBin] comes first - val swapTo = it.indexOf(songs[indexAtMusicBin]) + val swapTo = it.indexOf(playlist[indexAtMusicBin]) val tmp = it[swapTo] it[swapTo] = it[0] it[0] = tmp } - else Array(songs.size - indexAtMusicBin) { offset -> + else Array(playlist.size - indexAtMusicBin) { offset -> val k = offset + indexAtMusicBin - songs[k] + playlist[k] } musicBin = ArrayList(newMusicBin.toList()) @@ -162,7 +176,7 @@ class TerrarumMusicGovernor : MusicGovernor() { private val musicStopHooks = ArrayList<(MusicContainer) -> Unit>() init { - queueDirectory(App.customMusicDir, true, "intermittent") + queueDirectory(App.customMusicDir, true, "intermittent", true) } @@ -175,7 +189,7 @@ class TerrarumMusicGovernor : MusicGovernor() { } init { - songs.forEach { + playlist.forEach { App.disposables.add(it) } ambients.forEach { (k, v) -> @@ -218,7 +232,7 @@ class TerrarumMusicGovernor : MusicGovernor() { val timeNow = System.currentTimeMillis() val trackThis = App.audioMixer.musicTrack.currentTrack - if (caller is TerrarumMusicGovernor) { + if (caller is TerrarumBackgroundMusicPlayer) { if (stopCaller == null) { // printdbg(this, "Caller: this, prev caller: $stopCaller, len: $pauseLen, obliging stop request") stopMusic0(trackThis, callStopMusicHook, pauseLen) @@ -316,7 +330,7 @@ class TerrarumMusicGovernor : MusicGovernor() { // start the song queueing if there is one to play if (firstTime) { firstTime = false - if (songs.isNotEmpty()) musicState = STATE_INTERMISSION + if (playlist.isNotEmpty()) musicState = STATE_INTERMISSION if (ambients.isNotEmpty()) ambState = STATE_INTERMISSION } @@ -334,7 +348,7 @@ class TerrarumMusicGovernor : MusicGovernor() { STATE_INTERMISSION -> { intermissionAkku += delta - if (intermissionAkku >= intermissionLength && songs.isNotEmpty()) { + if (intermissionAkku >= intermissionLength && playlist.isNotEmpty()) { intermissionAkku = 0f musicState = STATE_FIREPLAY } diff --git a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt index b155f16eb..65fdbaf29 100644 --- a/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt +++ b/src/net/torvald/terrarum/modulebasegame/TerrarumIngame.kt @@ -262,7 +262,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { override var gameFullyLoaded = false internal set - override val musicGovernor = TerrarumMusicGovernor() + override val backgroundMusicPlayer = TerrarumBackgroundMusicPlayer() ////////////// @@ -1006,7 +1006,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { oldSelectedWireRenderClass = selectedWireRenderClass } - musicGovernor.update(this, delta) + backgroundMusicPlayer.update(this, delta) //////////////////////// // ui-related updates // @@ -1795,7 +1795,7 @@ open class TerrarumIngame(batch: FlippingSpriteBatch) : IngameInstance(batch) { catch (e: IllegalArgumentException) {} } - musicGovernor.dispose() + backgroundMusicPlayer.dispose() super.dispose() } } diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt index 1697c1a9a..8c8b2e8c7 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureJukebox.kt @@ -1,6 +1,5 @@ package net.torvald.terrarum.modulebasegame.gameactors -import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.g2d.SpriteBatch import com.jme3.math.FastMath @@ -10,7 +9,6 @@ import net.torvald.terrarum.* import net.torvald.terrarum.App.printdbg import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZE import net.torvald.terrarum.TerrarumAppConfiguration.TILE_SIZED -import net.torvald.terrarum.audio.AudioMixer.Companion.DEFAULT_FADEOUT_LEN import net.torvald.terrarum.audio.audiobank.MusicContainer import net.torvald.terrarum.audio.TerrarumAudioMixerTrack import net.torvald.terrarum.audio.dsp.NullFilter @@ -21,7 +19,7 @@ import net.torvald.terrarum.gameactors.Hitbox import net.torvald.terrarum.gameactors.Lightbox import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.langpack.Lang -import net.torvald.terrarum.modulebasegame.TerrarumMusicGovernor +import net.torvald.terrarum.modulebasegame.TerrarumBackgroundMusicPlayer import net.torvald.terrarum.modulebasegame.gameitems.FixtureItemBase import net.torvald.terrarum.modulebasegame.gameitems.ItemFileRef import net.torvald.terrarum.modulebasegame.gameitems.MusicDiscHelper @@ -108,7 +106,7 @@ class FixtureJukebox : Electric, PlaysMusic { // supress the normal background music playback if (musicIsPlaying && !flagDespawn) { - (INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, true) + (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, true) } } @@ -127,27 +125,49 @@ class FixtureJukebox : Electric, PlaysMusic { printdbg(this, "Title: $title, artist: $artist") - musicNowPlaying = MusicContainer(title, musicFile.file()) { + val returnToInitialState = { unloadEffector(musicNowPlaying) discCurrentlyPlaying = null musicNowPlaying?.tryDispose() musicNowPlaying = null - - printdbg(this, "Stop music $title - $artist") - - // can't call stopDiscPlayback() because of the recursion - - (INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval()) - backLamp.currentFrame = 0 playMech.currentFrame = 0 } + musicNowPlaying = MusicContainer(title, musicFile.file()) { + printdbg(this, "Stop music $title - $artist") + + // can't call stopDiscPlayback() because of the recursion + (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval()) + } + discCurrentlyPlaying = index - App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, DEFAULT_FADEOUT_LEN / 2f) { + // FIXME the olde way -- must be replaced with one that utilises MusicService + /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, DEFAULT_FADEOUT_LEN / 2f) { startAudio(musicNowPlaying!!) { loadEffector(it) } - } + }*/ + + MusicService.playMusicalFixture( + // action: () -> Unit + { + startAudio(musicNowPlaying!!) { loadEffector(it) } + }, + // musicFinished: () -> Boolean + { + !musicIsPlaying + }, + // onSuccess: () -> Unit + { + + }, + // onFailure: (Throwable) -> Unit + { + + }, + // onFinally: () -> Unit + returnToInitialState + ) backLamp.currentFrame = 1 + (index / 2) @@ -165,7 +185,7 @@ class FixtureJukebox : Electric, PlaysMusic { */ fun stopGracefully() { stopDiscPlayback() - (INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval()) + (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval()) } diff --git a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt index d180cd573..d5964b70f 100644 --- a/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt +++ b/src/net/torvald/terrarum/modulebasegame/gameactors/FixtureMusicalTurntable.kt @@ -10,7 +10,7 @@ import net.torvald.terrarum.gameactors.AVKey import net.torvald.terrarum.gameitems.GameItem import net.torvald.terrarum.gameitems.ItemID import net.torvald.terrarum.langpack.Lang -import net.torvald.terrarum.modulebasegame.TerrarumMusicGovernor +import net.torvald.terrarum.modulebasegame.TerrarumBackgroundMusicPlayer import net.torvald.terrarum.modulebasegame.gameitems.FixtureItemBase import net.torvald.terrarum.modulebasegame.gameitems.ItemFileRef import net.torvald.terrarum.modulebasegame.gameitems.MusicDiscHelper @@ -96,7 +96,7 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { // supress the normal background music playback if (musicIsPlaying && !flagDespawn) { - (INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, true) + (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, true) } } @@ -111,21 +111,47 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { App.printdbg(this, "Title: $title, artist: $artist") - musicNowPlaying = MusicContainer(title, musicFile.file()) { + val returnToInitialState = { unloadEffector(musicNowPlaying) musicNowPlaying?.tryDispose() musicNowPlaying = null + } + + musicNowPlaying = MusicContainer(title, musicFile.file()) { App.printdbg(this, "Stop music $title - $artist") // can't call stopDiscPlayback() because of the recursion - - (INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval()) + (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval()) } - App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 2f) { + + // FIXME the olde way -- must be replaced with one that utilises MusicService + /*App.audioMixer.requestFadeOut(App.audioMixer.musicTrack, AudioMixer.DEFAULT_FADEOUT_LEN / 2f) { startAudio(musicNowPlaying!!) { loadEffector(it) } - } + }*/ + + MusicService.playMusicalFixture( + // action: () -> Unit + { + startAudio(musicNowPlaying!!) { loadEffector(it) } + }, + // musicFinished: () -> Boolean + { + !musicIsPlaying + }, + // onSuccess: () -> Unit + { + + }, + // onFailure: (Throwable) -> Unit + { + + }, + // onFinally: () -> Unit + returnToInitialState + ) + (sprite as SheetSpriteAnimation).currentRow = 0 } @@ -137,7 +163,7 @@ class FixtureMusicalTurntable : Electric, PlaysMusic { */ fun stopGracefully() { stopDiscPlayback() - (INGAME.musicGovernor as TerrarumMusicGovernor).stopMusic(this, pauseLen = (INGAME.musicGovernor as TerrarumMusicGovernor).getRandomMusicInterval()) + (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).stopMusic(this, pauseLen = (INGAME.backgroundMusicPlayer as TerrarumBackgroundMusicPlayer).getRandomMusicInterval()) } diff --git a/src/net/torvald/terrarum/transaction/Transaction.kt b/src/net/torvald/terrarum/transaction/Transaction.kt new file mode 100644 index 000000000..8fd9b98a1 --- /dev/null +++ b/src/net/torvald/terrarum/transaction/Transaction.kt @@ -0,0 +1,76 @@ +package net.torvald.terrarum.transaction + +/** + * Created by minjaesong on 2024-06-28. + */ +interface Transaction { + + /** + * Call this function to begin the transaction. + * + * When started using [TransactionListener.runTransaction], the transaction runs on a separate thread, + * and thus any operation that requires GL Context will fail. + */ + fun start(state: TransactionState) + + /** + * Called by [TransactionListener.runTransaction], when the transaction was successful. + */ + fun onSuccess(state: TransactionState) + + /** + * Called by [TransactionListener.runTransaction], when the transaction failed. + */ + fun onFailure(e: Throwable, state: TransactionState) + +} + +abstract class TransactionListener { + + /** `null` if not locked, a class that acquired the lock if locked */ + var transactionLockedBy: Any? = null; private set + val transactionLocked: Boolean; get() = (transactionLockedBy != null) + + + /** + * Transaction modifies a given state to a new state, then applies the new state to the object. + * The given `transaction` may only modify values which is passed to it. + */ + fun runTransaction(transaction: Transaction, onFinally: () -> Unit = {}) { + Thread { synchronized(this) { + val state = getCurrentStatusForTransaction() + if (!transactionLocked) { + try { + transaction.start(state) + // if successful: + commitTransaction(state) + // notify the success + transaction.onSuccess(state) + } + catch (e: Throwable) { + // if failed, notify the failure + transaction.onFailure(e, state) + } + finally { + onFinally() + } + } + else { + transaction.onFailure(LockedException(this, transactionLockedBy), state) + } + } }.start() + } + + protected abstract fun getCurrentStatusForTransaction(): TransactionState + protected abstract fun commitTransaction(state: TransactionState) +} + +class LockedException(listener: TransactionListener, lockedBy: Any?) : + Exception("Transaction is rejected because the class '${listener.javaClass.canonicalName}' is locked by '${lockedBy?.javaClass?.canonicalName}'") + +@JvmInline value class TransactionState(val valueTable: MutableMap) { + operator fun get(key: String) = valueTable[key] + operator fun set(key: String, value: Any?) { + valueTable[key] = value + } +} \ No newline at end of file diff --git a/src/net/torvald/util/ArrayListMap.kt b/src/net/torvald/util/ArrayListMap.kt index 411aa7de7..45a5e1504 100644 --- a/src/net/torvald/util/ArrayListMap.kt +++ b/src/net/torvald/util/ArrayListMap.kt @@ -96,11 +96,11 @@ class ArrayListMap : MutableMap { return super.computeIfAbsent(key, mappingFunction) } - override fun computeIfPresent(key: K, remappingFunction: BiFunction): V? { + override fun computeIfPresent(key: K, remappingFunction: BiFunction): V? { return super.computeIfPresent(key, remappingFunction) } - override fun merge(key: K, value: V, remappingFunction: BiFunction): V? { + override fun merge(key: K, value: V & Any, remappingFunction: BiFunction): V? { return super.merge(key, value, remappingFunction) }