transaction-based music playback managing wip

This commit is contained in:
minjaesong
2024-07-04 21:34:27 +09:00
parent 7582eae1ee
commit 69571b6a3f
15 changed files with 688 additions and 132 deletions

8
.idea/kotlinc.xml generated
View File

@@ -1,13 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="17" />
<option name="jvmTarget" value="21" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="1.8" />
<option name="languageVersion" value="1.8" />
<option name="apiVersion" value="1.9" />
<option name="languageVersion" value="1.9" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.8.0" />
<option name="version" value="1.9.23-release-779" />
</component>
</project>

View File

@@ -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() {

View File

@@ -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<MusicContainer>
get() = ingame.musicGovernor.extortField<List<MusicContainer>>("songs")!!
get() = ingame.backgroundMusicPlayer.extortField<List<MusicContainer>>("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,18 +347,12 @@ 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
MusicService.playPrevSongInPlaylist {
ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
iHitTheStopButton = false
stopRequested = false
}
}
}
// prev page in the playlist
else if (listViewPanelScroll == 1f) {
val scrollMax = ((currentlySelectedAlbum?.length
@@ -372,36 +368,44 @@ 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) }
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*/
MusicService.resumePlaylistPlayback {
iHitTheStopButton = false
stopRequested = false
}
}
}
}
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
MusicService.playNextSongInPlaylist {
ingame.backgroundMusicPlayer.startMusic(this) // required for "intermittent" mode
iHitTheStopButton = false
stopRequested = false
}
}
}
// next page in the playlist
else if (listViewPanelScroll == 1f) {
val scrollMax = ((currentlySelectedAlbum?.length
@@ -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
}

View File

@@ -1,6 +1,6 @@
package net.torvald.terrarum
open class MusicGovernor {
open class BackgroundMusicPlayer {
open fun update(ingameInstance: IngameInstance, delta: Float) {

View File

@@ -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) {

View File

@@ -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)
}
}

View File

@@ -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<MusicContainer>,
/** 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<Int>()
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
)
}
}
}

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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<MusicContainer> = emptyList()
private var playlist: List<MusicContainer> = 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
}

View File

@@ -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()
}
}

View File

@@ -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())
}

View File

@@ -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())
}

View File

@@ -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<String, Any?>) {
operator fun get(key: String) = valueTable[key]
operator fun set(key: String, value: Any?) {
valueTable[key] = value
}
}

View File

@@ -96,11 +96,11 @@ class ArrayListMap<K, V> : MutableMap<K, V> {
return super.computeIfAbsent(key, mappingFunction)
}
override fun computeIfPresent(key: K, remappingFunction: BiFunction<in K, in V, out V?>): V? {
override fun computeIfPresent(key: K, remappingFunction: BiFunction<in K, in V & Any, out V?>): V? {
return super.computeIfPresent(key, remappingFunction)
}
override fun merge(key: K, value: V, remappingFunction: BiFunction<in V, in V, out V?>): V? {
override fun merge(key: K, value: V & Any, remappingFunction: BiFunction<in V & Any, in V & Any, out V?>): V? {
return super.merge(key, value, remappingFunction)
}