2020-06-04 16:42:38 +09:30
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--
-- 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 " ,
2020-06-06 23:28:07 +09:30
author = " cake, trepan, Smoth, Licho, xponen, Birdulon " ,
date = " Mar 01, 2008, Aug 20 2009, Nov 23 2011, June 2020 " ,
2020-06-04 16:42:38 +09:30
license = " GNU GPL, v2 or later " ,
layer = 0 ,
enabled = true -- loaded by default?
}
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
2020-06-06 16:57:25 +09:30
options_path = " Settings/Audio "
2020-06-04 16:42:38 +09:30
options = {
useIncludedTracks = {
name = " Use Included Tracks " ,
2020-06-06 16:57:25 +09:30
type = " bool " ,
2020-06-04 16:42:38 +09:30
value = true ,
2020-06-06 16:57:25 +09:30
desc = " Use the tracks included with Zero-K " ,
2020-06-04 16:42:38 +09:30
noHotkey = true ,
} ,
pausemusic = {
2020-06-06 16:57:25 +09:30
name = " Pause Music " ,
type = " bool " ,
2020-06-04 16:42:38 +09:30
value = false ,
desc = " Music pauses with game " ,
noHotkey = true ,
} ,
2020-06-06 23:15:09 +09:30
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 ,
} ,
2020-06-06 23:28:07 +09:30
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 ,
} ,
2020-06-04 16:42:38 +09:30
}
local unitExceptions = include ( " Configs/snd_music_exception.lua " )
2020-06-06 23:28:07 +09:30
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
2020-10-30 14:19:56 +10:30
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
2020-06-06 23:28:07 +09:30
local war2Threshold = 300000
local warThreshold = 30000
2020-06-06 23:15:09 +09:30
local peaceThreshold = 10000
2020-06-06 16:57:25 +09:30
local PLAYLIST_FILE = " sounds/music/playlist.lua "
2020-06-06 23:15:09 +09:30
local LOOP_BUFFER = 0.015 -- if looping track is this close to the end, go ahead and loop
2020-06-04 16:42:38 +09:30
local UPDATE_PERIOD = 1
2020-06-06 16:57:25 +09:30
local musicType = " peace "
2020-06-05 19:47:02 +09:30
local warPointsIter = 1 -- Position in circular buffer. 1-indexed because L[ew]a
2020-06-06 23:15:09 +09:30
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 = { }
2020-06-05 19:47:02 +09:30
local warPointsRollover = 4000000000 -- Roll back to zero after this many have accumulated
2020-06-04 16:42:38 +09:30
local timeframetimer = 0
local timeframetimer_short = 0
2020-06-06 16:57:25 +09:30
local loopTrack = " "
local previousTrack = " "
2020-06-06 23:15:09 +09:30
local prevMusicType = " "
2020-06-04 16:42:38 +09:30
local newTrackWait = 1000
2020-06-06 23:15:09 +09:30
local numVisibleFriendly = 0
2020-06-04 16:42:38 +09:30
local numVisibleEnemy = 0
local fadeVol
2020-06-05 19:47:02 +09:30
local curTrac = " no name "
local songText = " no name "
2020-06-04 16:42:38 +09:30
local haltMusic = false
local looping = false
local paused = false
local lastTrackTime = - 1
2020-06-06 18:28:48 +09:30
local tracks = { }
2020-06-04 16:42:38 +09:30
2020-10-30 14:19:56 +10:30
local firstTime = true
2020-06-04 16:42:38 +09:30
local wasPaused = false
local firstFade = true
local initSeed = 0
local initialized = false
2020-10-30 14:19:56 +10:30
local gameStarted = Spring.GetGameFrame ( ) > 30 --0
2020-06-04 16:42:38 +09:30
local gameOver = false
local myTeam = Spring.GetMyTeamID ( )
local isSpec = Spring.GetSpectatingState ( ) or Spring.IsReplay ( )
local defeat = false
2020-10-30 14:19:56 +10:30
local timesStartTrack = 0
2020-06-04 16:42:38 +09:30
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
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
2020-06-06 16:57:25 +09:30
musicType = " custom "
2020-06-05 19:47:02 +09:30
2020-06-04 16:42:38 +09:30
curTrack = trackInit
loopTrack = trackLoop
2020-10-30 14:19:56 +10:30
Spring.StopSoundStream ( )
2020-06-04 16:42:38 +09:30
Spring.PlaySoundStream ( trackInit , WG.music_volume or 0.5 )
looping = 0.5
end
2020-10-30 14:19:56 +10:30
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
2020-06-04 16:42:38 +09:30
local function StartTrack ( track )
2020-10-30 14:19:56 +10:30
timesStartTrack = timesStartTrack + 1
Spring.Echo ( " StartTrack called with mood: " .. musicType .. " ( " .. tostring ( timesStartTrack ) .. " ) " )
2020-06-06 16:57:25 +09:30
if not tracks.peace then
Spring.Echo ( " Missing tracks.peace file, no music started " )
2020-06-04 16:42:38 +09:30
return
end
local newTrack = previousTrack
2020-06-06 16:57:25 +09:30
if musicType == " custom " then
2020-06-06 23:15:09 +09:30
prevMusicType = " peace "
2020-06-04 16:42:38 +09:30
musicType = " peace "
end
2020-06-06 16:57:25 +09:30
if ( not gameStarted ) then
2020-10-30 14:19:56 +10:30
prevMusicType = " briefing "
2020-06-06 16:57:25 +09:30
musicType = " briefing "
end
2020-10-30 14:19:56 +10:30
haltMusic = false
looping = false
2020-06-04 16:42:38 +09:30
if track then
2020-06-06 16:57:25 +09:30
newTrack = track -- play specified track
musicType = " custom "
2020-06-04 16:42:38 +09:30
else
2020-06-06 16:57:25 +09:30
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
2020-06-04 16:42:38 +09:30
end
2020-06-06 16:57:25 +09:30
2020-06-04 16:42:38 +09:30
firstFade = false
previousTrack = newTrack
curTrack = newTrack
2020-10-30 14:19:56 +10:30
Spring.StopSoundStream ( )
2020-06-06 23:15:09 +09:30
Spring.PlaySoundStream ( newTrack , WG.music_volume or 0.5 )
2020-06-05 19:47:02 +09:30
2020-06-04 16:42:38 +09:30
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
2020-06-06 18:28:48 +09:30
function InitializeTracks ( )
Spring.Echo ( " Initializing music tracks " )
2020-06-06 23:28:07 +09:30
-- 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
2020-06-06 16:57:25 +09:30
local vfsMode = ( options.useIncludedTracks . value and VFS.RAW_FIRST ) or VFS.RAW
2020-06-06 23:28:07 +09:30
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
2020-06-06 16:57:25 +09:30
end
2020-06-06 23:15:09 +09:30
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
2020-06-04 16:42:38 +09:30
end
2020-06-06 23:15:09 +09:30
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
2020-06-06 23:28:07 +09:30
local warPoints = totalKilled + totalDmg
2020-10-30 14:19:56 +10:30
if ( warPoints >= options.war1Threshold . value ) then
musicType = " war "
if ( warPoints >= options.war2Threshold . value ) then musicType = " war2 " end
if ( not isSpec ) then
2020-06-06 23:28:07 +09:30
if attritionRatio < options.attritionRatioLosing . value then
2020-06-06 23:15:09 +09:30
musicType = " losing "
2020-06-06 23:28:07 +09:30
elseif attritionRatio > options.attritionRatioWinning . value then
2020-06-06 23:15:09 +09:30
musicType = " winning "
end
end
2020-10-30 14:19:56 +10:30
else --if (warPoints <= peaceThreshold) then
musicType = " peace "
2020-06-06 23:15:09 +09:30
end
2020-10-30 14:19:56 +10:30
end
2020-06-06 23:15:09 +09:30
2020-10-30 14:19:56 +10:30
function UpdateTick ( )
newTrackWait = newTrackWait + 1
if moodDynamic [ musicType ] then
EvaluateMood ( )
2020-06-06 23:15:09 +09:30
end
local playedTime , totalTime = Spring.GetSoundStreamTime ( )
2020-10-30 14:19:56 +10:30
if ( ( prevMusicType ~= musicType ) and ( moodPriorities [ musicType ] >= moodPriorities [ prevMusicType ] ) )
2020-06-06 23:15:09 +09:30
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
2020-10-30 14:19:56 +10:30
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
2020-06-04 16:42:38 +09:30
if not initialized then
math.randomseed ( os.clock ( ) * 100 )
initialized = true
2020-06-06 23:15:09 +09:30
-- these are here to give epicmenu time to set the values properly (else it's always default at startup)
2020-06-06 16:57:25 +09:30
InitializeTracks ( )
2020-06-04 16:42:38 +09:30
end
2020-06-05 19:47:02 +09:30
2020-06-04 16:42:38 +09:30
timeframetimer_short = timeframetimer_short + dt
if timeframetimer_short > 0.03 then
2020-06-06 23:15:09 +09:30
CheckLoop ( )
2020-06-04 16:42:38 +09:30
timeframetimer_short = 0
end
2020-06-05 19:47:02 +09:30
2020-06-04 16:42:38 +09:30
timeframetimer = timeframetimer + dt
if ( timeframetimer > UPDATE_PERIOD ) then -- every second
2020-10-30 14:19:56 +10:30
UpdateTick ( )
2020-06-04 16:42:38 +09:30
timeframetimer = 0
end
end
2020-10-30 14:19:56 +10:30
-- function widget:GameStart()
-- if not gameStarted then
-- gameStarted = true
-- prevMusicType = musicType
-- musicType = "peace"
-- StartTrack()
-- end
--
-- newTrackWait = 0
-- end
2020-06-04 16:42:38 +09:30
-- Safety of a heisenbug
2020-10-30 14:19:56 +10:30
-- function widget:GameFrame()
-- widget:GameStart()
-- widgetHandler:RemoveCallIn("GameFrame")
-- end
2020-06-04 16:42:38 +09:30
function widget : UnitDamaged ( unitID , unitDefID , unitTeam , damage , paralyzer )
2020-06-05 19:47:02 +09:30
if unitExceptions [ unitDefID ] then return end
2020-06-04 16:42:38 +09:30
if ( UnitDefs [ unitDefID ] == nil ) then return end
2020-06-05 19:47:02 +09:30
if paralyzer then return end
2020-06-06 23:28:07 +09:30
if spAreTeamsAllied ( unitTeam or 0 , myTeam ) then
2020-06-06 23:15:09 +09:30
dmgPointsFriendly [ warPointsIter ] = dmgPointsFriendly [ warPointsIter ] + damage
2020-06-04 16:42:38 +09:30
else
2020-06-06 23:15:09 +09:30
dmgPointsHostile [ warPointsIter ] = dmgPointsHostile [ warPointsIter ] + damage
2020-06-04 16:42:38 +09:30
end
end
function widget : UnitDestroyed ( unitID , unitDefID , teamID )
2020-06-05 19:47:02 +09:30
if unitExceptions [ unitDefID ] then return end
2020-06-06 23:15:09 +09:30
local unitCost = UnitDefs [ unitDefID ] . metalCost
2020-06-05 19:47:02 +09:30
2020-06-06 23:28:07 +09:30
if spAreTeamsAllied ( teamID or 0 , myTeam ) then
2020-06-06 23:15:09 +09:30
deathPointsFriendly [ warPointsIter ] = deathPointsFriendly [ warPointsIter ] + unitCost
2020-06-05 19:47:02 +09:30
else
2020-06-06 23:15:09 +09:30
deathPointsHostile [ warPointsIter ] = deathPointsHostile [ warPointsIter ] + unitCost
2020-06-05 19:47:02 +09:30
end
2020-06-04 16:42:38 +09:30
end
function widget : TeamDied ( team )
if team == myTeam and not isSpec then
defeat = true
end
end
local function PlayGameOverMusic ( gameWon )
2020-10-30 14:19:56 +10:30
gameOver = true
2020-06-04 16:42:38 +09:30
if gameWon then
musicType = " victory "
else
musicType = " defeat "
end
2020-10-30 14:19:56 +10:30
StartTrack ( )
2020-06-04 16:42:38 +09:30
end
function widget : GameOver ( )
PlayGameOverMusic ( not defeat )
end
function widget : Initialize ( )
WG.Music = WG.Music or { }
2020-10-30 14:19:56 +10:30
-- WG.Music.StartTrack = StartTrack
-- WG.Music.StartLoopingTrack = StartLoopingTrack
-- WG.Music.StopTrack = StopTrack
-- WG.Music.SetWarThreshold = SetWarThreshold
-- WG.Music.SetPeaceThreshold = SetPeaceThreshold
-- WG.Music.GetMusicType = GetMusicType
2020-06-04 16:42:38 +09:30
WG.Music . PlayGameOverMusic = PlayGameOverMusic
2020-06-06 16:57:25 +09:30
-- for TrackName,TrackDef in pairs(tracks.peace) do
2020-06-04 16:42:38 +09:30
-- 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
2020-06-05 19:47:02 +09:30
2020-06-06 23:28:07 +09:30
for i = 1 , warPointsSize do
2020-06-06 23:15:09 +09:30
dmgPointsFriendly [ i ] = 0
dmgPointsHostile [ i ] = 0
deathPointsFriendly [ i ] = 0
deathPointsHostile [ i ] = 0
2020-06-04 16:42:38 +09:30
end
end
function widget : Shutdown ( )
Spring.StopSoundStream ( )
WG.Music = nil
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------