mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-17 21:53:46 +03:00
Upload
This commit is contained in:
801
gamemodes/ixhl2rp/plugins/better_music_radio/sv_hooks.lua
Normal file
801
gamemodes/ixhl2rp/plugins/better_music_radio/sv_hooks.lua
Normal file
@@ -0,0 +1,801 @@
|
||||
--[[
|
||||
| This file was obtained through the combined efforts
|
||||
| of Madbluntz & Plymouth Antiquarian Society.
|
||||
|
|
||||
| Credits: lifestorm, Gregory Wayne Rossel JR.,
|
||||
| Maloy, DrPepper10 @ RIP, Atle!
|
||||
|
|
||||
| Visit for more: https://plymouth.thetwilightzone.ru/
|
||||
--]]
|
||||
|
||||
local PLUGIN = PLUGIN
|
||||
--[[
|
||||
LUA RADIO DJ!
|
||||
;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;
|
||||
; ;
|
||||
; ;
|
||||
; ;
|
||||
; ;
|
||||
; ;
|
||||
; ;
|
||||
; ;
|
||||
,;;;;; ,;;;;;
|
||||
;;;;;; ;;;;;;
|
||||
`;;;;' `;;;;'
|
||||
]]
|
||||
|
||||
ix.musicRadio = ix.musicRadio or {}
|
||||
ix.musicRadio.channels = ix.musicRadio.channels or {}
|
||||
-- a table for managing which radio is listening to which channel, basically
|
||||
-- allows us to sync up the musics
|
||||
ix.musicRadio.destinations = ix.musicRadio.destinations or {}
|
||||
ix.musicRadio.staticTime = 3 -- how long does static play for between tunings?
|
||||
ix.musicRadio.transitionTime = 3 -- how long should the transitions between songs last for?
|
||||
ix.musicRadio.radioVolume = 0.7 -- DEFAULT volume between 0 to 1 of the radios
|
||||
ix.musicRadio.radioLevel = 70 -- DEFAULT volume in db of the radios
|
||||
ix.musicRadio.spookMinTime = 60 * 60 -- 1 hour (minimum time between spooky sfx)
|
||||
ix.musicRadio.spookChance = 99999 -- probability (/second) of spooky sfx playing (after minimum time)
|
||||
ix.musicRadio.dspPreset = 2 -- ROOM EMPTY SMALL BRIGHT https://maurits.tv/data/garrysmod/wiki/wiki.garrysmod.com/index67df-2.html
|
||||
|
||||
hook.Add("EntityRemoved", "StopMusicRadioSound", function(ent)
|
||||
-- there is an undocumented engine bug which causes a memory leak here
|
||||
-- it will cause a black hole to open as soon as someone removes a music radio
|
||||
-- it will grow until EVERYTHING IS CONSUMED
|
||||
if (ent.soundCache) then
|
||||
for _, snd in pairs(ent.soundCache) do
|
||||
if (snd and snd.Stop) then
|
||||
snd:Stop()
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
function ix.musicRadio:ChannelIsValid(chName)
|
||||
if (!chName or !self.channels[chName] or !istable(self.channels[chName])) then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function ix.musicRadio:ClassIsValid(class)
|
||||
if (!self.chanList[class] or !istable(self.chanList[class])) then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
-- called to totally reinitialize all the channels on X class of the music radios
|
||||
function ix.musicRadio:RestartClass(class)
|
||||
if (!self.chanList[class]) then
|
||||
ErrorNoHaltWithStack("Attempted to restart uninitialized class: "..tostring(class))
|
||||
return
|
||||
end
|
||||
|
||||
if (!istable(self.chanList[class])) then
|
||||
ErrorNoHaltWithStack("Attempted to restart class which has no channels: "..tostring(class))
|
||||
return
|
||||
end
|
||||
|
||||
for _, chName in ipairs(self.chanList[class]) do
|
||||
self:RestartChannel(chName)
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:RestartChannel(chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
ErrorNoHaltWithStack("Attempted to restart uninitialized channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
-- pause the channel, which will stop the current song on all the entities listening to it
|
||||
self:PauseDestination(chName, true)
|
||||
|
||||
-- destroy the timer so we can start it again
|
||||
if (self.destinations[chName].timerName) then
|
||||
timer.Remove(self.destinations[chName].timerName)
|
||||
end
|
||||
|
||||
-- now rebuild the channel timer
|
||||
self:StartDestination(chName)
|
||||
end
|
||||
|
||||
-- called when a music radio tunes into a specific channel
|
||||
function ix.musicRadio:TuneIn(ent, chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!self.destinations[chName] or
|
||||
!self:DestinationHasTimer(chName) or
|
||||
!self.destinations[chName].curSong
|
||||
) then
|
||||
self.destinations[chName] = {}
|
||||
self:StartDestination(chName)
|
||||
end
|
||||
|
||||
self.destinations[chName][ent:EntIndex()] = ent
|
||||
ix.musicRadio:SyncEnt(chName, ent)
|
||||
|
||||
self:PlayTuneStatic(ent)
|
||||
end
|
||||
|
||||
-- called when a music radio entity turns off
|
||||
-- and/or switches off of their current station
|
||||
function ix.musicRadio:TuneOut(ent, chName, otherChName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
local entIndex = ent:EntIndex()
|
||||
if (!self.destinations[chName][entIndex]) then
|
||||
return
|
||||
end
|
||||
|
||||
if (otherChName) then
|
||||
if (!self.destinations[otherChName]) then
|
||||
self.destinations[otherChName] = {}
|
||||
self:StartDestination(otherChName)
|
||||
end
|
||||
|
||||
self.destinations[otherChName][entIndex] = ent -- < get updates on the new channel
|
||||
ix.musicRadio:SyncEnt(otherChName, ent) -- < start playing the next song
|
||||
end
|
||||
|
||||
-- stop the current song
|
||||
local curSong = self.destinations[chName].curSong
|
||||
if (cursong and
|
||||
self.destinations[chName][entIndex].soundCache[curSong]
|
||||
) then
|
||||
if (!self.destinations[chName][entIndex].soundCache[curSong]:IsPlaying()) then
|
||||
self.destinations[chName][entIndex].soundCache[curSong]:Stop()
|
||||
end
|
||||
end
|
||||
|
||||
self.destinations[chName][entIndex] = nil -- < no longer recieve updates on the current channel
|
||||
end
|
||||
|
||||
-- starts timers that will auto play synced music for said channel
|
||||
-- or unpauses them if theyre stopped
|
||||
function ix.musicRadio:StartDestination(chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
if (self.destinations[chName] and self.destinations[chName].paused) then
|
||||
if (!self.destinations[chName].timerName) then
|
||||
ErrorNoHaltWithStack("Attempted to unpause uninitialized channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
timer.UnPause(self.destinations[chName].timerName)
|
||||
end
|
||||
|
||||
if (self.destinations[chName].timerName and
|
||||
timer.Exists(self.destinations[chName].timerName)) then
|
||||
return
|
||||
end
|
||||
|
||||
-- initialize some vars for tracking things
|
||||
self.destinations[chName].songsToNextAnn = math.random(8, 12)
|
||||
self.destinations[chName].songsSinceLastAnn = 0
|
||||
self.destinations[chName].ticksSinceLastSong = 0
|
||||
self.destinations[chName].ticksSinceLastSpook = 0
|
||||
self.destinations[chName].curSongLength = 0
|
||||
|
||||
self.destinations[chName].timerName = "MusicRadioSyncTimer"..chName
|
||||
timer.Create(self.destinations[chName].timerName, 1, 0, function()
|
||||
self:TickChanRunTimer(chName)
|
||||
end)
|
||||
end
|
||||
|
||||
function ix.musicRadio:DestinationHasTimer(chName)
|
||||
if (!self.destinations[chName].timerName) then
|
||||
return false
|
||||
end
|
||||
|
||||
return timer.Exists(self.destinations[chName].timerName)
|
||||
end
|
||||
|
||||
-- should tick every second that a channel is running to keep everything synced up!
|
||||
function ix.musicRadio:TickChanRunTimer(chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
self:CleanupNullEntities(chName)
|
||||
self:TickSpook(chName)
|
||||
|
||||
self.destinations[chName].ticksSinceLastSong = self.destinations[chName].ticksSinceLastSong + 1
|
||||
local songTimeWithTrans = self.destinations[chName].curSongLength or 0- self.transitionTime
|
||||
if (self.destinations[chName].ticksSinceLastSong >= songTimeWithTrans or
|
||||
!self.destinations[chName].curSong) then
|
||||
|
||||
self:PlayNextSong(chName)
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:TickSpook(chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
local class = self.channels[chName].class
|
||||
if (!class) then
|
||||
ErrorNoHaltWithStack("Cannot check state of spooky sounds: Channel has invalid classname!")
|
||||
return
|
||||
end
|
||||
|
||||
if (!self:ClassIsEligibleForSpookySounds(class)) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!self.destinations[chName].ticksSinceLastSpook) then
|
||||
self.destinations[chName].ticksSinceLastSpook = 0
|
||||
end
|
||||
|
||||
self.destinations[chName].ticksSinceLastSpook = self.destinations[chName].ticksSinceLastSpook + 1
|
||||
|
||||
if (self.destinations[chName].ticksSinceLastSpook < self.spookMinTime) then
|
||||
return
|
||||
end
|
||||
|
||||
if (math.random(1, self.spookChance) != 1) then
|
||||
return
|
||||
end
|
||||
|
||||
self.destinations[chName].ticksSinceLastSpook = 0
|
||||
|
||||
self:PlaySpookySound(chName)
|
||||
end
|
||||
|
||||
function ix.musicRadio:PlaySpookySound(chName)
|
||||
local snd = self:GetNextSpook()
|
||||
for idx, _ in pairs(self.destinations[chName]) do
|
||||
if (!isnumber(idx)) then
|
||||
continue
|
||||
end
|
||||
local ent = Entity(idx)
|
||||
if (ent and IsEntity(ent)) then
|
||||
if (ent.GetVolume and ent.GetLevel) then -- is a music radio (just double checking <3)
|
||||
self:InterruptCurrentSong(ent, snd.fname, snd.length, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:ClassIsEligibleForSpookySounds(class)
|
||||
if (!class or !self.spooky or !self.spooky.classes) then
|
||||
return false
|
||||
end
|
||||
|
||||
return self.spooky.classes[class]
|
||||
end
|
||||
|
||||
function ix.musicRadio:GetNextSpook()
|
||||
if (!self.spooky or !self.spooky.sounds) then
|
||||
ErrorNoHaltWithStack("Attempted to roll next spooky sound when no spooky sounds were initialized!")
|
||||
return
|
||||
end
|
||||
|
||||
--return self.spooky.sounds[math.random(1, #self.spooky.sounds)]
|
||||
return self.spooky.sounds[7]
|
||||
end
|
||||
|
||||
function ix.musicRadio:CleanupNullEntities(chName)
|
||||
--[[
|
||||
Fixes null entities subscribed to the channel
|
||||
]]
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
for idx, ent in pairs(self.destinations[chName]) do
|
||||
local _idx = tonumber(idx)
|
||||
if (_idx) then
|
||||
if (!IsValid(Entity(_idx))) then
|
||||
self.destinations[chName][idx] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:PlaySoundOnClass(soundName, className, length)
|
||||
if (!soundName or string.len(soundName) < 1) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!className or string.len(className) < 1 or !self.chanList[className]) then
|
||||
return
|
||||
end
|
||||
|
||||
if (length < 1) then
|
||||
return
|
||||
end
|
||||
|
||||
for i, chanName in ipairs(self.chanList[className]) do
|
||||
if (self.destinations[chanName]) then
|
||||
self.destinations[chanName].songsSinceLastAnn = self.destinations[chanName].songsSinceLastAnn + 1
|
||||
|
||||
self:PlayOnAllEnts(chanName, soundName)
|
||||
|
||||
self.destinations[chanName].curSong = soundName
|
||||
self.destinations[chanName].curSongLength = length
|
||||
self.destinations[chanName].ticksSinceLastSong = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:SeekClass(className)
|
||||
for i, chanName in ipairs(self.chanList[className]) do
|
||||
if (self.destinations[chanName]) then
|
||||
self:PlayNextSong(chanName)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:PlayNextSong(chName)
|
||||
-- current song over, time to start another one ;)
|
||||
-- or we haven't started a song yet
|
||||
-- reminder: the songs have baked in fade in/out which is why we do it this way
|
||||
local snd
|
||||
|
||||
-- get class ;)
|
||||
local class = self.channels[chName].class
|
||||
if (!class) then
|
||||
ErrorNoHaltWithStack("Cannot play next song: Channel has invalid classname!")
|
||||
return
|
||||
end
|
||||
|
||||
-- check if we're disabled
|
||||
if (self:GetClassShouldPlayStatic(class)) then
|
||||
snd = self.static.sounds[math.random(1, #self.static.sounds)]
|
||||
|
||||
-- check to see if its time for an announcement ;)
|
||||
elseif (self.destinations[chName].songsSinceLastAnn >= self.destinations[chName].songsToNextAnn) then
|
||||
-- its time!
|
||||
snd = self:GetNextAnnouncement(chName)
|
||||
if (snd) then
|
||||
self.destinations[chName].songsSinceLastAnn = 0
|
||||
self.destinations[chName].songsToNextAnn = math.random(8, 12)
|
||||
else
|
||||
-- no announcement for this class
|
||||
snd = self:GetNextSong(chName)
|
||||
self.destinations[chName].songsSinceLastAnn = self.destinations[chName].songsSinceLastAnn + 1
|
||||
end
|
||||
else
|
||||
-- its not time yet, play a song instead ;(
|
||||
snd = self:GetNextSong(chName)
|
||||
self.destinations[chName].songsSinceLastAnn = self.destinations[chName].songsSinceLastAnn + 1
|
||||
end
|
||||
|
||||
self:PlayOnAllEnts(chName, snd.fname)
|
||||
|
||||
self.destinations[chName].curSong = snd.fname
|
||||
self.destinations[chName].curSongLength = snd.length
|
||||
self.destinations[chName].ticksSinceLastSong = 0
|
||||
end
|
||||
|
||||
-- returns the next announcement as it exists in the channel list. aka:
|
||||
-- { fname = "filename", length = 420 seconds }
|
||||
function ix.musicRadio:GetNextAnnouncement(chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!self.channels[chName].class) then
|
||||
ErrorNoHaltWithStack("Attempted to roll next announcement on channel with no class: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
local class = self.channels[chName].class
|
||||
if (!self.announcements[class] or !self.announcements[class].sounds) then
|
||||
return
|
||||
end
|
||||
|
||||
local nextSnd
|
||||
-- prevent the same song from playing twice:
|
||||
for i=1, 10 do
|
||||
nextSnd = self.announcements[class].sounds[math.random(1,
|
||||
table.Count(self.announcements[class].sounds)) or 1]
|
||||
|
||||
if (nextSnd.fname != self.channels[chName].curSong) then
|
||||
return nextSnd
|
||||
end
|
||||
end
|
||||
|
||||
-- just reroll one more time and hope it isn't the same song ;)
|
||||
return self.announcements[class].sounds[math.random(1, self.announcements[class].sounds)]
|
||||
end
|
||||
|
||||
-- pause destination timers, which can be started again with self:StartDestination
|
||||
function ix.musicRadio:PauseDestination(chName, bHardStop)
|
||||
if (bHardStop == nil) then
|
||||
bHardStop = true
|
||||
end
|
||||
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!self.destinations[chName] or self.destinations[chName].paused) then
|
||||
ErrorNoHaltWithStack("1Attempted to pause uninitialized channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
if (!self.destinations[chName].timerName) then
|
||||
ErrorNoHaltWithStack("2Attempted to pause uninitialized channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
timer.Pause(self.destinations[chName].timerName)
|
||||
if (bHardStop) then
|
||||
local curSong = self.destinations[chName].curSong
|
||||
for idx, _ in pairs(self.destinations[chName]) do
|
||||
if (!isnumber(idx)) then
|
||||
continue
|
||||
end
|
||||
local ent = Entity(idx)
|
||||
if (ent.soundCache and ent.soundCache[curSong]) then
|
||||
if (ent.soundCache[curSong]:IsPlaying()) then
|
||||
ent.soundCache[curSong]:Stop()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.destinations[chName].ticksSinceLastSong = 0
|
||||
self.destinations[chName].curSongLength = 0
|
||||
end
|
||||
end
|
||||
|
||||
-- play sound for specified entity at the current time; sync
|
||||
-- because we have no way to 'seek' the track forward, just start at the beginning
|
||||
-- and then it'll sync up when the next song plays
|
||||
function ix.musicRadio:SyncEnt(chName, ent)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
return
|
||||
end
|
||||
if (self.destinations[chName].curSong) then
|
||||
self:PlayOnEnt(chName, self.destinations[chName].curSong, ent,
|
||||
ent:GetVolume() or self.radioVolume, ent:GetLevel() or self.radioLevel)
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:PlayOnEnt(chName, fileName, ent, vol, db)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
ErrorNoHaltWithStack("Attempt to play on entity in invalid channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
if (!fileName or string.len(fileName) < 1) then
|
||||
ErrorNoHaltWithStack("Invalid filename provided: "..tostring(fileName))
|
||||
return
|
||||
end
|
||||
|
||||
if (!ent or !IsValid(ent) or !ent:IsValid()) then
|
||||
ErrorNoHaltWithStack("Invalid entity provided to channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
local idx = ent:EntIndex()
|
||||
if (!self.destinations[chName][idx]) then
|
||||
ErrorNoHaltWithStack("Attempt to play on entity not in channel! ID: "..tostring(idx))
|
||||
return
|
||||
end
|
||||
|
||||
if (!self.destinations[chName][idx].soundCache or
|
||||
!istable(self.destinations[chName][idx].soundCache)
|
||||
) then
|
||||
self.destinations[chName][idx].soundCache = {}
|
||||
end
|
||||
|
||||
local curSound = self.destinations[chName][idx].curSound -- string filename of the sound.
|
||||
if (curSound and
|
||||
self.destinations[chName][idx].soundCache[curSound] and -- string filename indexes the sound cache
|
||||
self.destinations[chName][idx].soundCache[curSound]:IsPlaying()
|
||||
) then
|
||||
-- fade out the current track..
|
||||
self.destinations[chName][idx].soundCache[curSound]:FadeOut(self.transitionTime)
|
||||
end
|
||||
|
||||
if (self.destinations[chName][idx].soundCache[fileName]) then
|
||||
-- cache hit
|
||||
if (!self.destinations[chName][idx].soundCache[fileName]:IsPlaying()) then
|
||||
-- stop the song if its already playing
|
||||
self.destinations[chName][idx].soundCache[fileName]:Stop()
|
||||
end
|
||||
else
|
||||
-- cache miss
|
||||
|
||||
-- by default CreateSound networks with the PAS of the sound
|
||||
-- instead, we want to network it to everyone
|
||||
self.destinations[chName][idx].soundCache[fileName] = CreateSound(ent,
|
||||
fileName, RecipientFilter():AddAllPlayers())
|
||||
|
||||
-- clean the cache
|
||||
for snd, csnd in pairs(self.destinations[chName][idx].soundCache) do
|
||||
if (csnd != curSound and snd != fileName and !csnd:IsPlaying()) then
|
||||
self.destinations[chName][idx].soundCache[snd] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.destinations[chName][idx].soundCache[fileName]:SetSoundLevel(ent:GetLevel() or db)
|
||||
--[[
|
||||
NOTE: There is a memory issue in SetDSP that FP knows about but refuses to fix!!!!
|
||||
If there are too many CSoundPatches with an active DSP set on them playing in one room it will overrun the client's audio buffer.
|
||||
This makes the game run at like 2fps and it sounds like someone put a microphone in a blender.
|
||||
]]
|
||||
self.destinations[chName][idx].soundCache[fileName]:SetDSP(self.dspPreset)
|
||||
|
||||
self.destinations[chName][idx].soundCache[fileName]:PlayEx(ent:GetVolume() or vol, 100)
|
||||
|
||||
self.destinations[chName][idx].curSound = fileName
|
||||
end
|
||||
|
||||
-- play sound for all chan ents listening to a particular channel
|
||||
function ix.musicRadio:PlayOnAllEnts(chName, fileName, vol, db)
|
||||
if (!fileName or string.len(fileName) < 1) then
|
||||
ErrorNoHaltWithStack("Attempt to play empty or nil filename!")
|
||||
return
|
||||
end
|
||||
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
ErrorNoHaltWithStack("Attempt to play with invalid channel: "..tostring(chName))
|
||||
return
|
||||
end
|
||||
|
||||
for idx, _ in pairs(self.destinations[chName]) do
|
||||
if (!isnumber(idx)) then
|
||||
continue
|
||||
end
|
||||
local ent = Entity(idx)
|
||||
if (ent and IsEntity(ent)) then
|
||||
if (ent.GetVolume and ent.GetLevel) then -- is a music radio
|
||||
ix.musicRadio:PlayOnEnt(chName, fileName, ent,
|
||||
ent:GetVolume() or vol or self.radioVolume,
|
||||
ent:GetLevel() or db or self.radioLevel)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- returns the next song as it exists in the channel list. aka:
|
||||
-- { fname = "filename", length = 420 seconds }
|
||||
function ix.musicRadio:GetNextSong(chName)
|
||||
if (!self:ChannelIsValid(chName)) then
|
||||
ErrorNoHaltWithStack("Cannot play next song: Channel is invalid!")
|
||||
return
|
||||
end
|
||||
|
||||
local nextSong
|
||||
-- prevent the same song from playing twice:
|
||||
for i=1, 10 do
|
||||
nextSong = self.channels[chName].songs[math.random(1, #self.channels[chName].songs)]
|
||||
if (nextSong.fname != self.channels[chName].curSong) then
|
||||
return nextSong
|
||||
end
|
||||
end
|
||||
|
||||
-- just reroll one more time and hope it isn't the same song ;)
|
||||
return self.channels[chName].songs[math.random(1, #self.channels[chName].songs)]
|
||||
end
|
||||
|
||||
-- plays a random splurt of static on the musicradio entity
|
||||
function ix.musicRadio:PlayTuneStatic(ent, maxVol)
|
||||
if (!ent or !ent.EmitSound) then
|
||||
return
|
||||
end
|
||||
local staticFName = "willardnetworks/musicradio/musicradio_static_"..tostring(math.random(1, 6)..".mp3")
|
||||
|
||||
self:InterruptCurrentSong(ent, staticFName, self.staticTime, maxVol)
|
||||
end
|
||||
|
||||
function ix.musicRadio:InterruptCurrentSong(ent, fName, time, maxVol)
|
||||
local vol = ent:GetVolume() or self.radioVolume
|
||||
if (maxVol) then
|
||||
vol = 1
|
||||
end
|
||||
|
||||
local chan = ent:GetNWString("curChan", "")
|
||||
local curVol = ent:GetNWInt("vol", 1)
|
||||
local curLvl = ent:GetNWInt("db", 70)
|
||||
ent:SetNWInt("vol", 0.1) -- just in case ;)
|
||||
local curSound = self.destinations[chan].curSong
|
||||
if (ent.soundCache) then
|
||||
ent.soundCache[curSound]:ChangeVolume(0.05, 1)
|
||||
ent.soundCache[curSound]:SetSoundLevel(60)
|
||||
end
|
||||
|
||||
ent:EmitSound(fName,
|
||||
ent.db or self.radioLevel, 100, vol)
|
||||
|
||||
timer.Simple(time, function()
|
||||
if (ent and IsValid(ent)) then
|
||||
ent:StopSound(fName)
|
||||
|
||||
if (!ent.soundCache) then
|
||||
return
|
||||
end
|
||||
|
||||
chan = ent:GetNWString("curChan", "")
|
||||
local curSoundNow = self.destinations[chan].curSong
|
||||
ent:SetNWInt("vol", curVol)
|
||||
ent.soundCache[curSoundNow]:ChangeVolume(curVol, 1) -- turn the old song back up
|
||||
ent.soundCache[curSoundNow]:SetSoundLevel(curLvl)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function ix.musicRadio:InstallTuner(client, isPirate)
|
||||
local char = client:GetCharacter()
|
||||
local trace = client:GetEyeTraceNoCursor()
|
||||
local ent = trace.Entity
|
||||
|
||||
if (!ent or !IsValid(ent)) then
|
||||
client:Notify("You are not looking at anything!")
|
||||
return
|
||||
end
|
||||
|
||||
if (!ent.SetVolume or !ent.GetRadioClass) then
|
||||
client:Notify("The device you are looking at is not a music radio!")
|
||||
return
|
||||
end
|
||||
|
||||
local curCls = ent:GetRadioClass()
|
||||
if (isPirate and curCls == "pirate") then
|
||||
client:Notify("This radio is already tuned to the pirate frequencies!")
|
||||
return
|
||||
elseif (!isPirate and curCls == "benefactor") then
|
||||
client:Notify("This radio is already tuned to the benefactor frequencies!")
|
||||
return
|
||||
end
|
||||
|
||||
local lvl = char:GetSkill("crafting")
|
||||
if (lvl < 10) then
|
||||
client:Notify("Your crafting skill is too low to perform this action.")
|
||||
return
|
||||
end
|
||||
|
||||
local cls = isPirate and "pirate" or "benefactor"
|
||||
-- 'turn off' the radio
|
||||
ent:StopCurrentSong()
|
||||
ix.musicRadio:TuneOut(ent, ent:GetChan())
|
||||
ent:SetNWString("curChan", "")
|
||||
|
||||
-- set the new class
|
||||
ent:SetRadioClass(cls)
|
||||
|
||||
client:SetAction("Tuning...", 3, function()
|
||||
-- tune it to the new class
|
||||
ent:SetNWString("curChan", ent.defaultStation)
|
||||
ix.musicRadio:TuneIn(ent, ent.defaultStation)
|
||||
end)
|
||||
end
|
||||
|
||||
local function OnSave(entity, data)
|
||||
if (!IsValid(entity) or !entity.GetRadioClass) then
|
||||
ErrorNoHaltWithStack("Attempted to save invalid entity as a radio!")
|
||||
return
|
||||
end
|
||||
|
||||
data.radioClass = entity:GetRadioClass() or "benefactor"
|
||||
end
|
||||
|
||||
local function OnRestore(entity, data)
|
||||
if (!IsValid(entity) or !entity.GetRadioClass) then
|
||||
ErrorNoHaltWithStack("Attempted to restore invalid entity as a radio!")
|
||||
return
|
||||
end
|
||||
|
||||
if (!data.radioClass) then
|
||||
data.radioClass = "benefactor"
|
||||
end
|
||||
|
||||
entity:SetRadioClass(data.radioClass)
|
||||
end
|
||||
|
||||
function PLUGIN:RegisterSaveEnts()
|
||||
ix.saveEnts:RegisterEntity("wn_musicradio", true, true, true, {
|
||||
OnSave = OnSave,
|
||||
OnRestore = OnRestore
|
||||
})
|
||||
end
|
||||
|
||||
-- Called when loading all the data that has been saved.
|
||||
function PLUGIN:LoadData()
|
||||
if (!ix.config.Get("SaveEntsOldLoadingEnabled")) then return end
|
||||
|
||||
if (!istable(ix.musicRadio.static)) then
|
||||
ix.musicRadio.static = {}
|
||||
end
|
||||
|
||||
-- might as well load up the static classes here too
|
||||
ix.musicRadio.static.classes = ix.musicRadio:GetSavedStatic()
|
||||
|
||||
for _, v in ipairs(ix.data.Get("musicRadios") or {}) do
|
||||
local entity = ents.Create(v.class)
|
||||
entity:SetPos(v.pos)
|
||||
entity:SetAngles(v.angles)
|
||||
entity:Spawn()
|
||||
|
||||
entity:SetSolid(SOLID_OBB)
|
||||
entity:PhysicsInit(SOLID_OBB)
|
||||
|
||||
local physObj = entity:GetPhysicsObject()
|
||||
|
||||
if (IsValid(physObj)) then
|
||||
physObj:EnableMotion(false)
|
||||
physObj:Sleep()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:GetClassShouldPlayStatic(className)
|
||||
if (!self:ClassIsValid(className)) then
|
||||
ErrorNoHaltWithStack("Attempted to get static state on invalid class: "..tostring(className))
|
||||
return
|
||||
end
|
||||
|
||||
if (!istable(self.static)) then
|
||||
self:InitStatic()
|
||||
end
|
||||
|
||||
if (!istable(self.static.classes)) then
|
||||
self.static.classes = ix.musicRadio:GetSavedStatic()
|
||||
end
|
||||
|
||||
if (self.static.classes[className] == nil) then
|
||||
-- hasn't been set yet
|
||||
self.static.classes[className] = self.CHAN_ENABLED
|
||||
self:SaveStatic() -- save the new default
|
||||
end
|
||||
|
||||
if (self.static.classes[className] == self.CHAN_DISABLED) then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- sets a class of radio as 'disabled' so that it only plays static.
|
||||
function ix.musicRadio:SetClassStaticState(className, state)
|
||||
if (!self:ClassIsValid(className)) then
|
||||
ErrorNoHaltWithStack("Attempted to set static state on invalid class: "..tostring(className))
|
||||
return
|
||||
end
|
||||
|
||||
self.static.classes[className] = state
|
||||
|
||||
if (state == self.CHAN_DISABLED) then
|
||||
-- immediately start playing static on all the radios of this class
|
||||
self:PlaySoundOnClass(self.static.sounds[math.random(1, #self.static.sounds)].fname,
|
||||
className, 55)
|
||||
else
|
||||
self:RestartClass(className) -- otherwise, restart the classes channels
|
||||
end
|
||||
|
||||
self:SaveStatic()
|
||||
end
|
||||
|
||||
-- get the static classes from data folder
|
||||
function ix.musicRadio:GetSavedStatic()
|
||||
local jsonDat = file.Read("musicradio/static.json", "DATA")
|
||||
local tab = util.JSONToTable(jsonDat or "{}")
|
||||
|
||||
if (!tab or !istable(tab)) then
|
||||
ErrorNoHaltWithStack("Unable to load saved musicradio static")
|
||||
return
|
||||
end
|
||||
|
||||
return tab
|
||||
end
|
||||
|
||||
-- save the current state to data folder
|
||||
function ix.musicRadio:SaveStatic()
|
||||
local tab = util.TableToJSON(self.static.classes or {})
|
||||
file.CreateDir("musicradio")
|
||||
file.Write("musicradio/static.json", tab)
|
||||
end
|
||||
Reference in New Issue
Block a user