Zero-K_Custom_Widgets/snd_music.lua

472 lines
14 KiB
Lua
Raw Normal View History

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
--
-- 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",
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"
options = {
useIncludedTracks = {
name = "Use Included Tracks",
2020-06-06 16:57:25 +09:30
type = "bool",
value = true,
2020-06-06 16:57:25 +09:30
desc = "Use the tracks included with Zero-K",
noHotkey = true,
},
pausemusic = {
2020-06-06 16:57:25 +09:30
name = "Pause Music",
type = "bool",
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,
},
}
local unitExceptions = include("Configs/snd_music_exception.lua")
2020-06-06 23:28:07 +09:30
local spAreTeamsAllied = Spring.AreTeamsAllied
local spGetUnitRulesParam = Spring.GetUnitRulesParam
2020-06-06 23:28:07 +09:30
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
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
local UPDATE_PERIOD = 1
2020-06-06 16:57:25 +09:30
local musicType = "peace"
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 = {}
local warPointsRollover = 4000000000 -- Roll back to zero after this many have accumulated
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 = ""
local newTrackWait = 1000
2020-06-06 23:15:09 +09:30
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
2020-06-06 18:28:48 +09:30
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
2020-06-06 16:57:25 +09:30
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) .. ")")
2020-06-06 16:57:25 +09:30
if not tracks.peace then
Spring.Echo("Missing tracks.peace file, no music started")
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"
musicType = "peace"
end
2020-06-06 16:57:25 +09:30
if (not gameStarted) then
prevMusicType = "briefing"
2020-06-06 16:57:25 +09:30
musicType = "briefing"
end
haltMusic = false
looping = false
if track then
2020-06-06 16:57:25 +09:30
newTrack = track -- play specified track
musicType = "custom"
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
end
2020-06-06 16:57:25 +09:30
firstFade = false
previousTrack = newTrack
curTrack = newTrack
Spring.StopSoundStream()
2020-06-06 23:15:09 +09:30
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
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
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
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
else --if (warPoints <= peaceThreshold) then
musicType = "peace"
2020-06-06 23:15:09 +09:30
end
end
2020-06-06 23:15:09 +09: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()
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
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
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()
end
timeframetimer_short = timeframetimer_short + dt
if timeframetimer_short > 0.03 then
2020-06-06 23:15:09 +09:30
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
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
else
2020-06-06 23:15:09 +09:30
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
2020-06-06 23:15:09 +09:30
local unitCost = UnitDefs[unitDefID].metalCost
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
else
2020-06-06 23:15:09 +09:30
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
2020-06-06 16:57:25 +09:30
-- 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
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
end
end
function widget:Shutdown()
Spring.StopSoundStream()
WG.Music = nil
end
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------