MusicController tweaks and hooks for mood - War and Peace (#5364)

* MusicController tweaks and hooks for mood - War and Peace

* MusicController tweaks and hooks for mood - patch1

* MusicController tweaks and hooks for mood - const object

* MusicController tweaks and hooks for mood - patch2

Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
SomeTroglodyte 2021-10-03 10:56:27 +02:00 committed by GitHub
parent 982d739ec8
commit a0f6596ee8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 144 additions and 39 deletions

View File

@ -15,6 +15,7 @@ import com.unciv.models.tilesets.TileSetCache
import com.unciv.models.translations.Translations
import com.unciv.ui.LanguagePickerScreen
import com.unciv.ui.audio.MusicController
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.utils.*
import com.unciv.ui.worldscreen.PlayerReadyScreen
import com.unciv.ui.worldscreen.WorldScreen
@ -111,7 +112,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() {
// This stuff needs to run on the main thread because it needs the GL context
Gdx.app.postRunnable {
musicController.chooseTrack()
musicController.chooseTrack(suffix = MusicMood.Menu)
ImageGetter.ruleset = RulesetCache.getBaseRuleset() // so that we can enter the map editor without having to load a game first

View File

@ -16,6 +16,8 @@ import com.unciv.models.ruleset.Difficulty
import com.unciv.models.ruleset.ModOptionsConstants
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.models.ruleset.unit.BaseUnit
import java.util.*
import kotlin.math.min
@ -199,8 +201,12 @@ class GameInfo {
currentPlayerCiv = getCivilization(currentPlayer)
if (currentPlayerCiv.isSpectator()) currentPlayerCiv.popupAlerts.clear() // no popups for spectators
if (turns % 10 == 0) //todo measuring actual play time might be nicer
UncivGame.Current.musicController.chooseTrack(currentPlayerCiv.civName,
MusicMood.peaceOrWar(currentPlayerCiv.isAtWar()), MusicTrackChooserFlags.setNextTurn)
// Start our turn immediately before the player can made decisions - affects whether our units can commit automated actions and then be attacked immediately etc.
// Start our turn immediately before the player can make decisions - affects
// whether our units can commit automated actions and then be attacked immediately etc.
notifyOfCloseEnemyUnits(thisPlayer)
}

View File

@ -29,7 +29,7 @@ class MusicController {
private const val musicHistorySize = 8 // number of names to keep to avoid playing the same in short succession
private val fileExtensions = listOf("mp3", "ogg") // flac, opus, m4a... blocked by Gdx, `wav` we don't want
internal const val consoleLog = false
internal const val consoleLog = true
private fun getFile(path: String) =
if (musicLocation == FileType.External && Gdx.files.isExternalStorageAvailable)
@ -98,9 +98,10 @@ class MusicController {
fireOnChange()
}
private fun fireOnChange() {
if (onTrackChangeListener == null) return
val fileName = currentlyPlaying()
if (fileName.isEmpty()) {
onTrackChangeListener?.invoke(fileName)
fireOnChange(fileName)
return
}
val fileNameParts = fileName.split('/')
@ -108,7 +109,16 @@ class MusicController {
var trackName = fileNameParts[if (fileNameParts.size > 3 && fileNameParts[2] == "music") 3 else 1]
for (extension in fileExtensions)
trackName = trackName.removeSuffix(".$extension")
onTrackChangeListener?.invoke(modName + (if (modName.isEmpty()) "" else ": ") + trackName)
fireOnChange(modName + (if (modName.isEmpty()) "" else ": ") + trackName)
}
private fun fireOnChange(trackLabel: String) {
try {
onTrackChangeListener?.invoke(trackLabel)
} catch (ex: Throwable) {
if (consoleLog)
println("onTrackChange event invoke failed: ${ex.message}")
onTrackChangeListener = null
}
}
/**
@ -143,10 +153,13 @@ class MusicController {
// Next track - if top slot empty and a next exists, move it to top and start
current = next
next = null
if (!current!!.play())
state = ControllerState.Shutdown
else
if (!current!!.play()) {
// Retry another track if playback start fails, after an extended pause
ticksOfSilence = -silenceLengthInTicks - 1000
state = ControllerState.Silence
} else {
fireOnChange()
}
} // else wait for the thread of next.load() to finish
} else if (!current!!.isPlaying()) {
// normal end of track
@ -201,14 +214,17 @@ class MusicController {
(!flags.contains(MusicTrackChooserFlags.PrefixMustMatch) || it.nameWithoutExtension().startsWith(prefix))
&& (!flags.contains(MusicTrackChooserFlags.SuffixMustMatch) || it.nameWithoutExtension().endsWith(suffix))
}
// sort them by prefix match / suffix match / not last played / random
// randomize
.shuffled()
// sort them by prefix match / suffix match / not last played
.sortedWith(compareBy(
{ if (it.nameWithoutExtension().startsWith(prefix)) 0 else 1 }
, { if (it.nameWithoutExtension().endsWith(suffix)) 0 else 1 }
, { if (it.path() in musicHistory) 1 else 0 }
, { Random().nextInt() }))
// Then just pick the first one. Not as wasteful as it looks - need to check all names anyway
.firstOrNull()
)).firstOrNull()
// Note: shuffled().sortedWith(), ***not*** .sortedWith(.., Random)
// the latter worked with older JVM's, current ones *crash* you when a compare is not transitive.
}
//endregion
@ -216,9 +232,10 @@ class MusicController {
/** This tells the music controller about active mods - all are allowed to provide tracks */
fun setModList ( newMods: HashSet<String> ) {
//todo: Ensure this gets updated where appropriate.
// loadGame; newGame: Choose Map with Mods?; map editor...
// check against "ImageGetter.ruleset=" ?
// This is hooked in most places where ImageGetter.setNewRuleset is called.
// Changes in permanent audiovisual mods are effective without this notification.
// Only the map editor isn't hooked, so if we wish to play mod-nation-specific tunes in the
// editor when e.g. a starting location is picked, that will have to be added.
mods = newMods
}
@ -227,14 +244,15 @@ class MusicController {
* Called without parameters it will choose a new ambient music track and start playing it with fade-in/out.
* Will do nothing when no music files exist or the master volume is zero.
*
* @param prefix file name prefix, meant to represent **Context** - in most cases a Civ name or default "Ambient"
* @param suffix file name suffix, meant to represent **Mood** - e.g. Peace, War, Theme...
* @param prefix file name prefix, meant to represent **Context** - in most cases a Civ name
* @param suffix file name suffix, meant to represent **Mood** - e.g. Peace, War, Theme, Defeat, Ambient
* (Ambient is the default when a track ends and exists so War Peace and the others are not chosen in that case)
* @param flags a set of optional flags to tune the choice and playback.
* @return `true` = success, `false` = no match, no playback change
*/
fun chooseTrack (
prefix: String = "",
suffix: String = "",
suffix: String = "Ambient",
flags: EnumSet<MusicTrackChooserFlags> = EnumSet.noneOf(MusicTrackChooserFlags::class.java)
): Boolean {
if (baseVolume == 0f) return false
@ -289,6 +307,17 @@ class MusicController {
return true
}
/** Variant of [chooseTrack] that tries several moods ([suffixes]) until a match is chosen */
fun chooseTrack (
prefix: String = "",
suffixes: List<String>,
flags: EnumSet<MusicTrackChooserFlags> = EnumSet.noneOf(MusicTrackChooserFlags::class.java)
): Boolean {
for (suffix in suffixes) {
if (chooseTrack(prefix, suffix, flags)) return true
}
return false
}
/**
* Pause playback with fade-out
@ -346,7 +375,7 @@ class MusicController {
private fun shutdown() {
state = ControllerState.Idle
fireOnChange()
onTrackChangeListener = null
// keep onTrackChangeListener! OptionsPopup will want to know when we start up again
if (musicTimer != null) {
musicTimer!!.cancel()
musicTimer = null

View File

@ -0,0 +1,12 @@
package com.unciv.ui.audio
object MusicMood {
const val Theme = "Theme"
const val Peace = "Peace"
const val War = "War"
const val Defeat = "Defeat"
const val Menu = "Menu"
val themeOrPeace = listOf(Theme, Peace)
fun peaceOrWar(isAtWar: Boolean) = if (isAtWar) War else Peace
}

View File

@ -1,5 +1,7 @@
package com.unciv.ui.audio
import java.util.*
enum class MusicTrackChooserFlags {
/** Makes prefix parameter a mandatory match */
PrefixMustMatch,
@ -11,4 +13,17 @@ enum class MusicTrackChooserFlags {
PlaySingle,
/** directly choose the 'fallback' file for playback */
PlayDefaultFile,
;
companion object {
// EnumSet factories
/** EnumSet.of([PlayDefaultFile], [PlaySingle]) */
val setPlayDefault: EnumSet<MusicTrackChooserFlags> = EnumSet.of(PlayDefaultFile, PlaySingle)
/** EnumSet.of([PrefixMustMatch], [PlaySingle]) */
val setSelectNation: EnumSet<MusicTrackChooserFlags> = EnumSet.of(PrefixMustMatch)
/** EnumSet.of([PrefixMustMatch], [SuffixMustMatch]) */
val setSpecific: EnumSet<MusicTrackChooserFlags> = EnumSet.of(PrefixMustMatch, SuffixMustMatch)
/** EnumSet.of([PrefixMustMatch], [SlowFade]) */
val setNextTurn: EnumSet<MusicTrackChooserFlags> = EnumSet.of(PrefixMustMatch, SlowFade)
}
}

View File

@ -148,16 +148,24 @@ class MusicTrackController(private var volume: Float) {
if (!state.canPlay || music == null) {
throw IllegalStateException("MusicTrackController.play called on uninitialized instance")
}
// Unexplained observed exception: Gdx.Music.play fails with
// "Unable to allocate audio buffers. AL Error: 40964" (AL_INVALID_OPERATION)
// Approach: This track dies, parent controller will enter state Silence thus retry after a while.
if (tryPlay(music!!)) return true
state = State.Error
return false
}
private fun tryPlay(music: Music): Boolean {
return try {
music!!.volume = volume
if (!music!!.isPlaying) // for fade-over this could be called by the end of the previous track
music!!.play()
music.volume = volume
if (!music.isPlaying) // for fade-over this could be called by the end of the previous track
music.play()
true
} catch (ex: Exception) {
println("Exception playing music: ${ex.message}")
if (MusicController.consoleLog)
ex.printStackTrace()
state = State.Error
false
}
}

View File

@ -6,6 +6,8 @@ import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.metadata.GameSpeed
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.VictoryType
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.utils.*
class GameOptionsTable(
@ -187,9 +189,11 @@ class GameOptionsTable(
var desiredCiv = ""
if (gameParameters.mods.contains(it)) {
val modNations = RulesetCache[it]?.nations
if (modNations != null && modNations.size > 0) {
desiredCiv = modNations.keys.first()
}
if (modNations != null && modNations.size > 0) desiredCiv = modNations.keys.first()
val music = UncivGame.Current.musicController
if (!music.chooseTrack(it, MusicMood.Theme, MusicTrackChooserFlags.setSelectNation) && desiredCiv.isNotEmpty())
music.chooseTrack(desiredCiv, MusicMood.themeOrPeace, MusicTrackChooserFlags.setSelectNation)
}
updatePlayerPickerTable(desiredCiv)

View File

@ -18,6 +18,8 @@ import com.unciv.models.metadata.Player
import com.unciv.models.ruleset.Nation
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.mapeditor.GameParametersScreen
import com.unciv.ui.pickerscreens.PickerScreen
import com.unciv.ui.utils.*
@ -347,6 +349,9 @@ private class NationPickerPopup(
private fun returnSelected() {
if (selectedNation == null) return
UncivGame.Current.musicController.chooseTrack(selectedNation!!.name, MusicMood.themeOrPeace, MusicTrackChooserFlags.setSelectNation)
if (previousScreen is GameParametersScreen)
previousScreen.mapEditorScreen.tileMap.switchPlayersNation(
player,

View File

@ -21,15 +21,18 @@ import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.stats.Stat
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.civilopedia.CivilopediaScreen
import com.unciv.ui.tilegroups.CityButton
import com.unciv.ui.utils.*
import com.unciv.ui.utils.UncivTooltip.Companion.addTooltip
import kotlin.collections.ArrayList
import kotlin.math.floor
import kotlin.math.roundToInt
import com.unciv.ui.utils.AutoScrollPane as ScrollPane
class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
class DiplomacyScreen(val viewingCiv:CivilizationInfo): CameraStageBaseScreen() {
private val leftSideTable = Table().apply { defaults().pad(10f) }
private val rightSideTable = Table()
@ -694,6 +697,9 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
if (promisesTable != null) diplomacyTable.add(promisesTable).row()
}
UncivGame.Current.musicController.chooseTrack(otherCiv.civName,
MusicMood.peaceOrWar(viewingCiv.isAtWarWith(otherCiv)), MusicTrackChooserFlags.setSelectNation)
return diplomacyTable
}
@ -831,6 +837,7 @@ class DiplomacyScreen(val viewingCiv:CivilizationInfo):CameraStageBaseScreen() {
diplomacyManager.declareWar()
setRightSideFlavorText(otherCiv, otherCiv.nation.attacked, "Very well.")
updateLeftSideTable()
UncivGame.Current.musicController.chooseTrack(otherCiv.civName, MusicMood.War, MusicTrackChooserFlags.setSpecific)
}, this).open()
}
return declareWarButton

View File

@ -4,12 +4,16 @@ import com.badlogic.gdx.scenes.scene2d.ui.ScrollPane
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.civilization.*
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.models.translations.fillPlaceholders
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.trade.LeaderIntroTable
import com.unciv.ui.utils.*
import java.util.*
/**
* [Popup] communicating events other than trade offers to the player.
@ -47,6 +51,7 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu
}
init {
val music = UncivGame.Current.musicController
when (popupAlert.type) {
AlertType.WarDeclaration -> {
@ -58,12 +63,14 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu
responseTable.add(getCloseButton("You'll pay for this!"))
responseTable.add(getCloseButton("Very well."))
add(responseTable)
music.chooseTrack(civInfo.civName, MusicMood.War, MusicTrackChooserFlags.setSpecific)
}
AlertType.Defeated -> {
val civInfo = worldScreen.gameInfo.getCivilization(popupAlert.value)
addLeaderName(civInfo)
addGoodSizedLabel(civInfo.nation.defeated).row()
add(getCloseButton("Farewell."))
music.chooseTrack(civInfo.civName, MusicMood.Defeat, EnumSet.of(MusicTrackChooserFlags.SuffixMustMatch))
}
AlertType.FirstContact -> {
val civInfo = worldScreen.gameInfo.getCivilization(popupAlert.value)
@ -75,6 +82,7 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu
} else {
addGoodSizedLabel(nation.introduction).row()
add(getCloseButton("A pleasure to meet you."))
music.chooseTrack(civInfo.civName, MusicMood.themeOrPeace, MusicTrackChooserFlags.setSpecific)
}
}
AlertType.CityConquered -> {
@ -90,7 +98,7 @@ class AlertPopup(val worldScreen: WorldScreen, val popupAlert: PopupAlert): Popu
city.liberateCity(conqueringCiv)
worldScreen.shouldUpdate = true
close()
}
}
addLiberateOption(city.foundingCiv, liberateAction)
addSeparator()
}

View File

@ -3,6 +3,7 @@ package com.unciv.ui.worldscreen
import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.trade.TradeEvaluation
@ -10,6 +11,8 @@ import com.unciv.logic.trade.TradeLogic
import com.unciv.logic.trade.TradeOffer
import com.unciv.logic.trade.TradeType
import com.unciv.models.translations.tr
import com.unciv.ui.audio.MusicMood
import com.unciv.ui.audio.MusicTrackChooserFlags
import com.unciv.ui.trade.DiplomacyScreen
import com.unciv.ui.trade.LeaderIntroTable
import com.unciv.ui.utils.*
@ -34,22 +37,27 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){
val viewingCiv = worldScreen.viewingCiv
val tradeRequest = viewingCiv.tradeRequests.first()
init{
init {
val requestingCiv = worldScreen.gameInfo.getCivilization(tradeRequest.requestingCiv)
val nation = requestingCiv.nation
val trade = tradeRequest.trade
val isPeaceTreaty = trade.ourOffers.any { it.type == TradeType.Treaty && it.name == Constants.peaceTreaty }
val ourResources = viewingCiv.getCivResourcesByName()
val music = UncivGame.Current.musicController
if (isPeaceTreaty)
music.chooseTrack(nation.name, MusicMood.Peace, MusicTrackChooserFlags.setSpecific)
val leaderIntroTable = LeaderIntroTable(requestingCiv)
add(leaderIntroTable)
addSeparator()
val trade = tradeRequest.trade
val tradeOffersTable = Table().apply { defaults().pad(10f) }
tradeOffersTable.add("[${nation.name}]'s trade offer".toLabel())
// empty column to separate offers columns better
tradeOffersTable.add().pad(0f, 15f)
tradeOffersTable.add("Our trade offer".toLabel())
tradeOffersTable.row()
val ourResources = viewingCiv.getCivResourcesByName()
fun getOfferText(offer:TradeOffer): String {
var tradeText = offer.getOfferText()
@ -90,12 +98,14 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){
addButton("Not this time.", 'n') {
val diplomacyManager = requestingCiv.getDiplomacyManager(viewingCiv)
if(trade.ourOffers.all { it.type == TradeType.Luxury_Resource } && trade.theirOffers.all { it.type==TradeType.Luxury_Resource })
if (trade.ourOffers.all { it.type == TradeType.Luxury_Resource } && trade.theirOffers.all { it.type==TradeType.Luxury_Resource })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedLuxExchange,20) // offer again in 20 turns
if(trade.ourOffers.any { it.name == Constants.researchAgreement })
if (trade.ourOffers.any { it.name == Constants.researchAgreement })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedResearchAgreement,20) // offer again in 20 turns
if(trade.ourOffers.any { it.type == TradeType.Treaty && it.name == Constants.peaceTreaty })
diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace,5)
if (isPeaceTreaty) {
diplomacyManager.setFlag(DiplomacyFlags.DeclinedPeace, 5)
music.chooseTrack(nation.name, MusicMood.War, MusicTrackChooserFlags.setSpecific)
}
close()
requestingCiv.addNotification("[${viewingCiv.civName}] has denied your trade request", viewingCiv.civName, NotificationIcon.Trade)
@ -119,14 +129,14 @@ class TradePopup(worldScreen: WorldScreen): Popup(worldScreen){
viewingCiv.tradeRequests.remove(tradeRequest)
super.close()
}
class TradeThanksPopup(leaderIntroTable: LeaderIntroTable, worldScreen: WorldScreen): Popup(worldScreen) {
init {
add(leaderIntroTable)
addSeparator().padBottom(15f)
addGoodSizedLabel("Excellent!").row()
addCloseButton("Farewell.", KeyCharAndCode.SPACE) {
worldScreen.shouldUpdate=true
worldScreen.shouldUpdate = true
// in all cases, worldScreen.shouldUpdate should be set to true when we remove the last of the popups
// in order for the next trade to appear immediately
}

View File

@ -13,7 +13,6 @@ import com.unciv.UncivGame
import com.unciv.logic.MapSaver
import com.unciv.logic.civilization.PlayerType
import com.unciv.models.UncivSound
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.tilesets.TileSetCache
@ -420,7 +419,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
val music = previousScreen.game.musicController
music.setVolume(it)
if (!music.isPlaying())
music.chooseTrack(flags = EnumSet.of(MusicTrackChooserFlags.PlayDefaultFile, MusicTrackChooserFlags.PlaySingle))
music.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
}
add(musicVolumeSlider).pad(5f).row()
}
@ -467,6 +466,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
label.setText("Currently playing: [$it]".tr())
}
}
label.onClick { previousScreen.game.musicController.chooseTrack(flags = MusicTrackChooserFlags.setNextTurn) }
}
private fun Table.addDownloadMusic() {
@ -486,7 +486,7 @@ class OptionsPopup(val previousScreen: CameraStageBaseScreen) : Popup(previousSc
previousScreen.game.musicController.downloadDefaultFile()
Gdx.app.postRunnable {
tabs.replacePage("Sound", getSoundTab())
previousScreen.game.musicController.chooseTrack(flags = EnumSet.of(MusicTrackChooserFlags.PlayDefaultFile, MusicTrackChooserFlags.PlaySingle))
previousScreen.game.musicController.chooseTrack(flags = MusicTrackChooserFlags.setPlayDefault)
}
} catch (ex: Exception) {
Gdx.app.postRunnable {