-------------------------------------------------------------------------------- -------------------------------------------------------------------------------- -- -- 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 windows = {} local spAreTeamsAllied = Spring.AreTeamsAllied 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, [""]=true} -- Determines which music moods will instantly interrupt others, and which will wait for playing track to finish 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 = false local wasPaused = false local firstFade = true local initSeed = 0 local initialized = false local gameStarted = Spring.GetGameFrame() > 0 local gameOver = false local myTeam = Spring.GetMyTeamID() local isSpec = Spring.GetSpectatingState() or Spring.IsReplay() local defeat = false -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- 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 Spring.StopSoundStream() musicType = "custom" curTrack = trackInit loopTrack = trackLoop Spring.PlaySoundStream(trackInit, WG.music_volume or 0.5) looping = 0.5 end local function StartTrack(track) if not tracks.peace then Spring.Echo("Missing tracks.peace file, no music started") return end haltMusic = false looping = false Spring.StopSoundStream() local newTrack = previousTrack if musicType == "custom" then prevMusicType = "peace" musicType = "peace" end if (not gameStarted) then musicType = "briefing" end 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.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 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 function EvaluateMood() -- (Spring.GetGameRulesParam("recentNukeLaunch") == 1) -- Might need this for superweapon music later newTrackWait = newTrackWait + 1 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 moodDynamic[musicType] then if (warPoints >= options.war1Threshold.value) then musicType = "war" if (warPoints >= options.war2Threshold.value) then musicType = "war2" end if attritionRatio < options.attritionRatioLosing.value then musicType = "losing" elseif attritionRatio > options.attritionRatioWinning.value then musicType = "winning" end else --if (warPoints <= peaceThreshold) then musicType = "peace" end end if (not firstTime) then StartTrack() firstTime = true end local playedTime, totalTime = Spring.GetSoundStreamTime() -- playedTime = math.floor(playedTime) -- totalTime = math.floor(totalTime) --Spring.Echo(playedTime, totalTime, newTrackWait) --if((totalTime - playedTime) <= 6 and (totalTime >= 1) ) then --Spring.Echo("time left:", (totalTime - playedTime)) --Spring.Echo("volume:", (totalTime - playedTime)/6) --if ((totalTime - playedTime)/6 >= 0) then -- Spring.SetSoundStreamVolume((totalTime - playedTime)/6) --else -- Spring.SetSoundStreamVolume(0.1) --end --elseif(playedTime <= 5 )then--and not firstFade --Spring.Echo("time playing:", playedTime) --Spring.Echo("volume:", playedTime/5) --Spring.SetSoundStreamVolume( playedTime/5) --end --Spring.Echo(prevMusicType, musicType) 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 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 EvaluateMood() 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 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) local track if gameWon then if #tracks.victory <= 0 then return end track = tracks.victory[math.random(1, #tracks.victory)] musicType = "victory" else if #tracks.defeat <= 0 then return end track = tracks.defeat[math.random(1, #tracks.defeat)] musicType = "defeat" end looping = false Spring.StopSoundStream() Spring.PlaySoundStream(track, WG.music_volume or 0.5) WG.music_start_volume = WG.music_volume 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 for i=1,#windows do (windows[i]):Dispose() end end -------------------------------------------------------------------------------- --------------------------------------------------------------------------------