-------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -- -- file: gui_music.lua -- brief: yay music -- author: cake -- -- Copyright (C) 2007. -- Licensed under the terms of the GNU GPL, v2 or later. -- -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- function widget:GetInfo() return { name = "Music Player", desc = "Plays music based on situation", author = "cake, trepan, Smoth, Licho, xponen, Birdulon", date = "Mar 01, 2008, Aug 20 2009, Nov 23 2011, June 2020", license = "GNU GPL, v2 or later", layer = 0, enabled = true -- loaded by default? } end -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- options_path = "Settings/Audio" options = { useIncludedTracks = { name = "Use Included Tracks", type = "bool", value = true, desc = "Use the tracks included with Zero-K", noHotkey = true, }, pausemusic = { name = "Pause Music", type = "bool", value = false, desc = "Music pauses with game", noHotkey = true, }, attritionRatioLosing = { name = "Attrition Ratio: Losing", type = "number", value = 0.4, min = 0.1, max = 1.0, step = 0.05, desc = "Music switches to Losing when recent attrition ratio falls below this value", noHotkey = true, }, attritionRatioWinning = { name = "Attrition Ratio: Winning", type = "number", value = 3.0, min = 1.0, max = 8.0, step = 0.25, desc = "Music switches to Winning when recent attrition ratio rises above this value", noHotkey = true, }, war1Threshold = { name = "War Threshold 1", type = "number", value = 30000, min = 10000, max = 100000, step = 100, desc = "Music switches to War when recent war points rise above this value", noHotkey = true, }, war2Threshold = { name = "War Threshold 2", type = "number", value = 300000, min = 20000, max = 1000000, step = 100, desc = "Music switches to War2 when recent war points rise above this value", noHotkey = true, }, } local unitExceptions = include("Configs/snd_music_exception.lua") local spAreTeamsAllied = Spring.AreTeamsAllied local spGetUnitRulesParam = Spring.GetUnitRulesParam local MOODS = {"peace", "war", "war2", "winning", "losing", "briefing", "victory", "defeat"} local moodPriorities = {peace=0, war=1, war2=2, winning=4, losing=4, briefing=10, victory=10, defeat=10, [""]=0} -- Determines which music moods will instantly interrupt others, and which will wait for playing track to finish local moodDynamic = {peace=true, war=true, war2=true, winning=true, losing=true, briefing=false, victory=false, defeat=false, [""]=false} -- Determines which music moods will change dynamically local war2Threshold = 300000 local warThreshold = 30000 local peaceThreshold = 10000 local PLAYLIST_FILE = "sounds/music/playlist.lua" local LOOP_BUFFER = 0.015 -- if looping track is this close to the end, go ahead and loop local UPDATE_PERIOD = 1 local musicType = "peace" local warPointsIter = 1 -- Position in circular buffer. 1-indexed because L[ew]a local warPointsSize = 90 -- Size of circular buffer. Sampling is currently hardcoded but might change later. local dmgPointsFriendly = {} -- keeps track of the number of doods killed in each time frame local dmgPointsHostile = {} local deathPointsFriendly = {} -- metal costs of destroyed units local deathPointsHostile = {} local warPointsRollover = 4000000000 -- Roll back to zero after this many have accumulated local timeframetimer = 0 local timeframetimer_short = 0 local loopTrack = "" local previousTrack = "" local prevMusicType = "" local newTrackWait = 1000 local numVisibleFriendly = 0 local numVisibleEnemy = 0 local fadeVol local curTrac = "no name" local songText = "no name" local haltMusic = false local looping = false local paused = false local lastTrackTime = -1 local tracks = {} local firstTime = true local wasPaused = false local firstFade = true local initSeed = 0 local initialized = false local gameStarted = Spring.GetGameFrame() > 30 --0 local gameOver = false local myTeam = Spring.GetMyTeamID() local isSpec = Spring.GetSpectatingState() or Spring.IsReplay() local defeat = false local timesStartTrack = 0 -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- local function GetMusicType() return musicType end local function StartLoopingTrack(trackInit, trackLoop) if not (VFS.FileExists(trackInit) and VFS.FileExists(trackLoop)) then Spring.Log(widget:GetInfo().name, LOG.ERROR, "Missing one or both tracks for looping") end haltMusic = true musicType = "custom" curTrack = trackInit loopTrack = trackLoop Spring.StopSoundStream() Spring.PlaySoundStream(trackInit, WG.music_volume or 0.5) looping = 0.5 end function CheckLoop() local playedTime, totalTime = Spring.GetSoundStreamTime() paused = (playedTime == lastTrackTime) lastTrackTime = playedTime if looping then if looping == 0.5 then looping = 1 elseif playedTime >= totalTime - LOOP_BUFFER then Spring.StopSoundStream() Spring.PlaySoundStream(loopTrack, WG.music_volume or 0.5) end end end local function StartTrack(track) timesStartTrack = timesStartTrack + 1 Spring.Echo("StartTrack called with mood: " .. musicType .. " (" .. tostring(timesStartTrack) .. ")") if not tracks.peace then Spring.Echo("Missing tracks.peace file, no music started") return end local newTrack = previousTrack if musicType == "custom" then prevMusicType = "peace" musicType = "peace" end if (not gameStarted) then prevMusicType = "briefing" musicType = "briefing" end haltMusic = false looping = false if track then newTrack = track -- play specified track musicType = "custom" else local numTypeTracks = #tracks[musicType] if (numTypeTracks == 0) then return end for tries=1,10 do newTrack = tracks[musicType][math.random(1, numTypeTracks)] if newTrack ~= previousTrack then break end end end firstFade = false previousTrack = newTrack curTrack = newTrack Spring.StopSoundStream() Spring.PlaySoundStream(newTrack, WG.music_volume or 0.5) WG.music_start_volume = WG.music_volume end local function StopTrack(noContinue) looping = false Spring.StopSoundStream() if noContinue then haltMusic = true else haltMusic = false StartTrack() end end local function SetWarThreshold(num) if num and num >= 0 then warThreshold = num else warThreshold = 5000 end end local function SetPeaceThreshold(num) if num and num >= 0 then peaceThreshold = num else peaceThreshold = 1000 end end function InitializeTracks() Spring.Echo("Initializing music tracks") -- if VFS.FileExists(PLAYLIST_FILE, VFS.RAW_FIRST) then -- local plTracks = VFS.Include(PLAYLIST_FILE, nil, VFS.RAW_FIRST) -- tracks.war = plTracks.war -- tracks.peace = plTracks.peace -- tracks.briefing = plTracks.briefing -- tracks.victory = plTracks.victory -- tracks.defeat = plTracks.defeat -- end local vfsMode = (options.useIncludedTracks.value and VFS.RAW_FIRST) or VFS.RAW for i=1,#MOODS do local mood = MOODS[i] tracks[mood] = VFS.DirList("sounds/music/" .. mood .. "/", "*.ogg", vfsMode) -- tracks[mood] = tracks[mood] or VFS.DirList("sounds/music/" .. mood .. "/", "*.ogg", vfsMode) end end function EvaluateMood() -- (Spring.GetGameRulesParam("recentNukeLaunch") == 1) -- Might need this for superweapon music later numVisibleFriendly = 0 numVisibleEnemy = 0 local doods = Spring.GetVisibleUnits(-1, nil, true) for i=1,#doods do if Spring.IsUnitAllied(doods[i]) then numVisibleFriendly = numVisibleFriendly + 1 else numVisibleEnemy = numVisibleEnemy + 1 end end local totalKilled, friendliesKilled, hostilesKilled = 0, 0, 0 local totalDmg, friendlyDmg, hostileDmg = 0, 0, 0 local iLast = ((warPointsIter-16) % warPointsSize) + 1 -- Look back 15 periods. local iLast2 = ((warPointsIter-61) % warPointsSize) + 1 -- Look back 60 periods. -- Last 10 seconds count for double friendliesKilled = friendliesKilled + ((deathPointsFriendly[warPointsIter] - deathPointsFriendly[iLast]) % warPointsRollover) friendliesKilled = friendliesKilled + ((deathPointsFriendly[warPointsIter] - deathPointsFriendly[iLast2]) % warPointsRollover) hostilesKilled = hostilesKilled + ((deathPointsHostile[warPointsIter] - deathPointsHostile[iLast]) % warPointsRollover) hostilesKilled = hostilesKilled + ((deathPointsHostile[warPointsIter] - deathPointsHostile[iLast2]) % warPointsRollover) friendlyDmg = friendlyDmg + ((dmgPointsFriendly[warPointsIter] - dmgPointsFriendly[iLast]) % warPointsRollover) friendlyDmg = friendlyDmg + ((dmgPointsFriendly[warPointsIter] - dmgPointsFriendly[iLast2]) % warPointsRollover) hostileDmg = hostileDmg + ((dmgPointsHostile[warPointsIter] - dmgPointsHostile[iLast]) % warPointsRollover) hostileDmg = hostileDmg + ((dmgPointsHostile[warPointsIter] - dmgPointsHostile[iLast2]) % warPointsRollover) totalKilled = friendliesKilled + hostilesKilled local attritionRatio = (hostilesKilled+1)/(friendliesKilled+1) -- 1 metal is virtually nothing in the ratio, but this simplifies edge cases totalDmg = friendlyDmg + hostileDmg -- Roll to next index in the circular buffers, continue cumulative sum local iNext = (warPointsIter % warPointsSize) + 1 dmgPointsFriendly[iNext] = dmgPointsFriendly[warPointsIter] % warPointsRollover dmgPointsHostile[iNext] = dmgPointsHostile[warPointsIter] % warPointsRollover deathPointsFriendly[iNext] = deathPointsFriendly[warPointsIter] % warPointsRollover deathPointsHostile[iNext] = deathPointsHostile[warPointsIter] % warPointsRollover warPointsIter = iNext local warPoints = totalKilled + totalDmg if (warPoints >= options.war1Threshold.value) then musicType = "war" if (warPoints >= options.war2Threshold.value) then musicType = "war2" end if (not isSpec) then if attritionRatio < options.attritionRatioLosing.value then musicType = "losing" elseif attritionRatio > options.attritionRatioWinning.value then musicType = "winning" end end else --if (warPoints <= peaceThreshold) then musicType = "peace" end end function UpdateTick() newTrackWait = newTrackWait + 1 if moodDynamic[musicType] then EvaluateMood() end local playedTime, totalTime = Spring.GetSoundStreamTime() if ((prevMusicType ~= musicType) and (moodPriorities[musicType] >= moodPriorities[prevMusicType])) or (playedTime >= totalTime) -- both zero means track stopped and not(haltMusic or looping) then prevMusicType = musicType StartTrack() newTrackWait = 0 end local _, _, paused = Spring.GetGameSpeed() if (paused ~= wasPaused) and options.pausemusic.value then Spring.PauseSoundStream() wasPaused = paused end end function widget:Update(dt) if gameOver then return end if firstTime then musicType = "briefing" StartTrack() firstTime = false return end if ((not gameStarted) and (Spring.GetGameFrame() > 30)) then gameStarted = true prevMusicType = musicType musicType = "peace" StartTrack() newTrackWait = 0 end if not initialized then math.randomseed(os.clock()* 100) initialized=true -- these are here to give epicmenu time to set the values properly (else it's always default at startup) InitializeTracks() end timeframetimer_short = timeframetimer_short + dt if timeframetimer_short > 0.03 then CheckLoop() timeframetimer_short = 0 end timeframetimer = timeframetimer + dt if (timeframetimer > UPDATE_PERIOD) then -- every second UpdateTick() timeframetimer = 0 end end -- function widget:GameStart() -- if not gameStarted then -- gameStarted = true -- prevMusicType = musicType -- musicType = "peace" -- StartTrack() -- end -- -- newTrackWait = 0 -- end -- Safety of a heisenbug -- function widget:GameFrame() -- widget:GameStart() -- widgetHandler:RemoveCallIn("GameFrame") -- end function widget:UnitDamaged(unitID, unitDefID, unitTeam, damage, paralyzer) if unitExceptions[unitDefID] then return end if (UnitDefs[unitDefID] == nil) then return end if paralyzer then return end if spAreTeamsAllied(unitTeam or 0, myTeam) then dmgPointsFriendly[warPointsIter] = dmgPointsFriendly[warPointsIter] + damage else dmgPointsHostile[warPointsIter] = dmgPointsHostile[warPointsIter] + damage end end function widget:UnitDestroyed(unitID, unitDefID, teamID) if unitExceptions[unitDefID] then return end if spGetUnitRulesParam(unitID, "wasMorphedTo") then return end local unitCost = UnitDefs[unitDefID].metalCost if spAreTeamsAllied(teamID or 0, myTeam) then deathPointsFriendly[warPointsIter] = deathPointsFriendly[warPointsIter] + unitCost else deathPointsHostile[warPointsIter] = deathPointsHostile[warPointsIter] + unitCost end end function widget:TeamDied(team) if team == myTeam and not isSpec then defeat = true end end local function PlayGameOverMusic(gameWon) gameOver = true if gameWon then musicType = "victory" else musicType = "defeat" end StartTrack() end function widget:GameOver() PlayGameOverMusic(not defeat) end function widget:Initialize() WG.Music = WG.Music or {} -- WG.Music.StartTrack = StartTrack -- WG.Music.StartLoopingTrack = StartLoopingTrack -- WG.Music.StopTrack = StopTrack -- WG.Music.SetWarThreshold = SetWarThreshold -- WG.Music.SetPeaceThreshold = SetPeaceThreshold -- WG.Music.GetMusicType = GetMusicType WG.Music.PlayGameOverMusic = PlayGameOverMusic -- for TrackName,TrackDef in pairs(tracks.peace) do -- Spring.Echo("Track: " .. TrackDef) -- end --math.randomseed(os.clock()* 101.01)--lurker wants you to burn in hell rgn -- for i=1,20 do Spring.Echo(math.random()) end for i=1,warPointsSize do dmgPointsFriendly[i] = 0 dmgPointsHostile[i] = 0 deathPointsFriendly[i] = 0 deathPointsHostile[i] = 0 end end function widget:Shutdown() Spring.StopSoundStream() WG.Music = nil end -------------------------------------------------------------------------------- --------------------------------------------------------------------------------