mirror of
https://github.com/lifestorm/wnsrc.git
synced 2025-12-17 13:53:45 +03:00
Upload
This commit is contained in:
491
gamemodes/ixhl2rp/plugins/better_music_radio/cl_hooks.lua
Normal file
491
gamemodes/ixhl2rp/plugins/better_music_radio/cl_hooks.lua
Normal file
@@ -0,0 +1,491 @@
|
||||
--[[
|
||||
| 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
|
||||
-- vars for the radio displays
|
||||
PLUGIN.pitch = 180
|
||||
PLUGIN.yaw = 270
|
||||
PLUGIN.roll = 270
|
||||
PLUGIN.x = 8.7
|
||||
PLUGIN.y = -14
|
||||
PLUGIN.z = 17
|
||||
|
||||
-- stores the vector that the signal lines are pointing to, per entity
|
||||
-- tracks update targets for lerping
|
||||
local mradioSigLines = {}
|
||||
local leftMostSigLine = Vector(56, 62, 0)
|
||||
local possibleSigLineTargets = {
|
||||
[1] = Vector(103, 62, 0),
|
||||
[2] = Vector(95, 55, 0),
|
||||
[3] = Vector(89, 52, 0),
|
||||
[4] = Vector(98, 57, 0),
|
||||
[5] = Vector(101, 59, 0),
|
||||
}
|
||||
|
||||
-- stores the X that the frequency cursor should be at
|
||||
-- stores data used for lerping on frequency changes
|
||||
local mradioFreqLines = {}
|
||||
|
||||
-- used to illuminate a 'hitbox' around the interactable 3d2d elements
|
||||
local hitboxTargets = {
|
||||
volume = {
|
||||
x = 69,
|
||||
y = 181,
|
||||
w = 25,
|
||||
h = 25,
|
||||
shouldDraw = false
|
||||
},
|
||||
onOff = {
|
||||
x = 160,
|
||||
y = 130,
|
||||
w = 40,
|
||||
h = 30,
|
||||
shouldDraw = false
|
||||
},
|
||||
dial = {
|
||||
x = 280,
|
||||
y = 170,
|
||||
w = 50,
|
||||
h = 50,
|
||||
shouldDraw = false
|
||||
},
|
||||
}
|
||||
|
||||
local function drawActiveHitbox()
|
||||
for _, hitbox in pairs(hitboxTargets) do
|
||||
if (hitbox.shouldDraw) then
|
||||
surface.SetDrawColor(180, 21, 0, 120)
|
||||
surface.DrawOutlinedRect(hitbox.x, hitbox.y, hitbox.w, hitbox.h, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- lerp between current and desired line position; draw
|
||||
local function drawMRadioSigLine(ent)
|
||||
local idx = ent:EntIndex()
|
||||
if (!mradioSigLines[idx] or !istable(mradioSigLines[idx])) then
|
||||
return
|
||||
end
|
||||
|
||||
local lineDat = mradioSigLines[idx]
|
||||
lineDat.lineLastV = LerpVector(math.Clamp((SysTime() - (lineDat.lastUpdate or SysTime())) / 15, 0, 1),
|
||||
lineDat.lineLastV or leftMostSigLine,
|
||||
lineDat.lineNextV or leftMostSigLine)
|
||||
|
||||
if (ent:IsPlayingMusic()) then
|
||||
render.DrawLine(Vector(83, 75, 0),
|
||||
lineDat.lineLastV, Color(255, 0, 0), true)
|
||||
end
|
||||
end
|
||||
|
||||
-- update the desired position of the line
|
||||
local function updateMRadioSigLine(ent)
|
||||
local idx = ent:EntIndex()
|
||||
if (!mradioSigLines[idx] or !istable(mradioSigLines[idx])) then
|
||||
mradioSigLines[idx] = {}
|
||||
mradioSigLines[idx].lineLastV = leftMostSigLine
|
||||
mradioSigLines[idx].lineNextV = possibleSigLineTargets[1]
|
||||
mradioSigLines[idx].lastUpdate = SysTime()
|
||||
end
|
||||
|
||||
if (ent:IsPlayingMusic()) then
|
||||
-- pick a random place towards the right to point to
|
||||
mradioSigLines[idx].lineNextV = possibleSigLineTargets[math.random(1,
|
||||
table.Count(possibleSigLineTargets))]
|
||||
|
||||
else
|
||||
mradioSigLines[idx].lineNextV = leftMostSigLine
|
||||
end
|
||||
|
||||
mradioSigLines[idx].lastUpdate = SysTime()
|
||||
end
|
||||
|
||||
local function drawMRadioDLight(ent, pos)
|
||||
if (ent:IsPlayingMusic()) then
|
||||
local dlight = DynamicLight(ent:EntIndex())
|
||||
if (dlight) then
|
||||
dlight.pos = pos
|
||||
dlight.r = 124
|
||||
dlight.g = 107
|
||||
dlight.b = 72
|
||||
dlight.brightness = 1
|
||||
dlight.decay = 256
|
||||
dlight.size = 128
|
||||
dlight.dietime = CurTime() + 1
|
||||
dlight.style = 6 -- strobe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function drawMRadioSigLevel(ent, pos, angle, scale, alpha)
|
||||
surface.SetTextColor(0, 0, 0, alpha)
|
||||
|
||||
-- top left background
|
||||
if (ent:IsPlayingMusic()) then
|
||||
surface.SetDrawColor(209, 179, 120)
|
||||
else
|
||||
surface.SetDrawColor(12, 12, 12)
|
||||
end
|
||||
|
||||
surface.DrawRect(46, 34, 76, 46)
|
||||
surface.SetDrawColor(0, 0, 0)
|
||||
surface.DrawOutlinedRect(46, 34, 76, 46, 2)
|
||||
|
||||
surface.SetTextPos(48, 35)
|
||||
surface.SetFont("radioSurfaceSm")
|
||||
surface.DrawText("SIGNAL LEVEL")
|
||||
|
||||
surface.SetTextPos(75, 65)
|
||||
surface.SetFont("radioSurfaceXs")
|
||||
surface.DrawText("dBm")
|
||||
|
||||
-- draw the db guides
|
||||
local curDBm, _x, _y, symb
|
||||
surface.SetDrawColor(0, 0, 0, alpha - 50)
|
||||
for i=1, 5 do
|
||||
curDBm = -30 + (10 * i) -- -20, -10, 0, +10, +20
|
||||
_x = 38 + (13 * i)
|
||||
|
||||
if (i == 3) then
|
||||
_y = 48
|
||||
elseif (i == 2 or i == 4) then
|
||||
_y = 50
|
||||
else
|
||||
_y = 52
|
||||
end
|
||||
|
||||
if (i >= 4) then
|
||||
symb = "+"
|
||||
elseif (i == 3) then
|
||||
symb = " " -- fix spacing for 0dbm
|
||||
else
|
||||
symb = ""
|
||||
end
|
||||
|
||||
surface.SetTextPos(_x, _y)
|
||||
surface.DrawText(symb..curDBm)
|
||||
end
|
||||
|
||||
-- draw the vert guidelines
|
||||
local markerAlpha = alpha - 50
|
||||
if (!ent:IsPlayingMusic()) then
|
||||
markerAlpha = 0
|
||||
end
|
||||
|
||||
for i=1, 20 do
|
||||
if (i > 10 and i <= 15) then
|
||||
surface.SetDrawColor(2, 220, 17, markerAlpha)
|
||||
elseif (i > 15) then
|
||||
surface.SetDrawColor(255, 0, 0, markerAlpha)
|
||||
else
|
||||
surface.SetDrawColor(14, 15, 41, markerAlpha)
|
||||
end
|
||||
|
||||
surface.DrawRect(52 + (3 * i), 61, 1, 2)
|
||||
end
|
||||
|
||||
surface.SetDrawColor(0, 0, 0, alpha)
|
||||
surface.DrawOutlinedRect(50, 46, 68, 30, 1)
|
||||
end
|
||||
|
||||
local function drawMRadioFreqDialLine(ent, pos, ang, scale, alpha, freq)
|
||||
if (!ent:IsPlayingMusic()) then return end
|
||||
|
||||
local idx = ent:EntIndex()
|
||||
if (!mradioFreqLines[idx] or !istable(mradioFreqLines[idx])) then
|
||||
return
|
||||
end
|
||||
|
||||
local lineDat = mradioFreqLines[idx]
|
||||
lineDat.lineLastX = Lerp(math.Clamp((SysTime() - (lineDat.lastUpdate or SysTime())) / 20, 0, 1),
|
||||
lineDat.lineLastX, lineDat.lineNextX)
|
||||
|
||||
surface.SetDrawColor(255, 0, 0, alpha + 40)
|
||||
surface.DrawRect(lineDat.lineLastX, 47, 4, 21)
|
||||
end
|
||||
|
||||
local function updateMRadioFreqDialLine(ent)
|
||||
local idx = ent:EntIndex()
|
||||
if (!mradioFreqLines[idx] or !istable(mradioFreqLines[idx])) then
|
||||
mradioFreqLines[idx] = {}
|
||||
mradioFreqLines[idx].lineLastX = 220
|
||||
mradioFreqLines[idx].lineNextX = 220
|
||||
mradioFreqLines[idx].lastUpdate = SysTime()
|
||||
end
|
||||
|
||||
if (ent:IsPlayingMusic()) then
|
||||
-- pick a random place towards the right to point to
|
||||
local ch = ent:GetChan()
|
||||
if (ch and string.len(ch) > 0) then
|
||||
local chan = ix.musicRadio:GetChannel(ch)
|
||||
if (chan and istable(chan)) then
|
||||
mradioFreqLines[idx].lineNextX = chan.freqMap.dispX or 220
|
||||
end
|
||||
end
|
||||
else
|
||||
mradioFreqLines[idx].lineNextX = 220
|
||||
end
|
||||
|
||||
mradioFreqLines[idx].lastUpdate = SysTime()
|
||||
end
|
||||
|
||||
local function drawMRadioOnOff(ent, pos, ang, scale, alpha)
|
||||
surface.SetDrawColor(30, 32, 28, 129)
|
||||
surface.DrawRect(166, 136, 30, 20)
|
||||
surface.SetDrawColor(0, 0, 0, 255)
|
||||
surface.DrawOutlinedRect(166, 136, 30, 20, 1)
|
||||
|
||||
surface.SetFont("radioSurfaceSmLight")
|
||||
|
||||
surface.SetTextColor(0, 0, 0)
|
||||
if (ent:IsPlayingMusic()) then
|
||||
surface.SetTextColor(40, 180, 0, alpha - 35)
|
||||
end
|
||||
surface.SetTextPos(171, 137)
|
||||
surface.DrawText("ON")
|
||||
end
|
||||
|
||||
local function drawMRadioVolume(ent, pos, ang, scale, alpha)
|
||||
surface.SetDrawColor(0, 0, 0, alpha - 70)
|
||||
|
||||
surface.SetFont("radioSurfaceSm")
|
||||
local function _drawMRadioVolumeButton(x, y, text, bOn)
|
||||
surface.SetTextColor(141, 141, 141, alpha - 30)
|
||||
surface.SetTextPos(x + 2, y - 1)
|
||||
|
||||
if (bOn and ent:IsPlayingMusic()) then
|
||||
surface.SetTextColor(35, 175, 23, alpha)
|
||||
end
|
||||
surface.DrawText(text)
|
||||
end
|
||||
|
||||
local onButton = ent:GetIlluminatedVolButton()
|
||||
local volButtons = {
|
||||
[1] = { x = 53, y = 202 },
|
||||
[2] = { x = 50, y = 191 },
|
||||
[3] = { x = 53, y = 180 },
|
||||
[4] = { x = 61, y = 170 },
|
||||
[5] = { x = 72, y = 164 },
|
||||
[6] = { x = 84, y = 166 },
|
||||
[7] = { x = 95, y = 172 },
|
||||
[8] = { x = 101, y = 181 },
|
||||
[9] = { x = 104, y = 191 },
|
||||
[10] = { x = 99, y = 202 },
|
||||
}
|
||||
|
||||
for key, button in ipairs(volButtons) do
|
||||
if (key == onButton) then
|
||||
_drawMRadioVolumeButton(button.x, button.y, tostring(key), true)
|
||||
else
|
||||
_drawMRadioVolumeButton(button.x, button.y, tostring(key), false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function drawMRadioSurface(ent, pos, ang, scale)
|
||||
local backlightAlphaPulse = 150
|
||||
if (ent:IsPlayingMusic()) then
|
||||
backlightAlphaPulse = 255 - math.Clamp(
|
||||
math.floor(math.sin(CurTime() * 120) * 100) - 10,
|
||||
10,
|
||||
30
|
||||
)
|
||||
end
|
||||
|
||||
drawMRadioSigLevel(ent, pos, angle, scale, backlightAlphaPulse)
|
||||
drawMRadioSigLine(ent)
|
||||
|
||||
-- large display center
|
||||
if (ent.IsPlayingMusic and ent:IsPlayingMusic()) then
|
||||
surface.SetDrawColor(209, 179, 120)
|
||||
else
|
||||
surface.SetDrawColor(12, 12, 12)
|
||||
end
|
||||
surface.DrawRect(160, 34, 340, 68)
|
||||
surface.SetDrawColor(10, 10, 10)
|
||||
surface.DrawOutlinedRect(160, 34, 340, 68, 2)
|
||||
|
||||
-- draw the frequency info
|
||||
surface.SetDrawColor(0, 0, 0, backlightAlphaPulse)
|
||||
surface.DrawOutlinedRect(180, 77, 280, 18, 2)
|
||||
surface.SetFont("radioSurfaceSm")
|
||||
local xStep = 50
|
||||
for i=1, 6 do
|
||||
surface.SetTextPos(135 + (xStep * i), 80)
|
||||
surface.DrawText(tostring(500 + (50 * i)))
|
||||
surface.DrawRect(135 + (xStep * i) + 10, 69, 4, 8) -- center
|
||||
if (i == 1) then
|
||||
-- draw first guideline
|
||||
surface.DrawRect(184, 73, 2, 4)
|
||||
end
|
||||
if (i < 6) then
|
||||
-- draw four guidelines right of current (unless at end)
|
||||
surface.DrawRect(135 + (xStep * i) + 22.5, 73, 2, 4)
|
||||
surface.DrawRect(135 + (xStep * i) + 35, 73, 2, 4)
|
||||
surface.DrawRect(135 + (xStep * i) + 47.5, 73, 2, 4)
|
||||
end
|
||||
end
|
||||
|
||||
surface.SetDrawColor(0, 0, 0, backlightAlphaPulse)
|
||||
surface.DrawOutlinedRect(180, 45, 280, 35, 2)
|
||||
|
||||
drawMRadioFreqDialLine(ent, pos, angle, scale, backlightAlphaPulse)
|
||||
-- draw the light from the backlight
|
||||
drawMRadioDLight(ent, ent:LocalToWorld(Vector(10, 5, 1)))
|
||||
|
||||
-- draw the toggle on/off button
|
||||
drawMRadioOnOff(ent, pos, ang, scale, backlightAlphaPulse)
|
||||
|
||||
-- draw the volume button
|
||||
drawMRadioVolume(ent, pos, ang, scale, backlightAlphaPulse)
|
||||
end
|
||||
|
||||
do
|
||||
-- step one: use consolas for everything
|
||||
-- step two: profit???
|
||||
surface.CreateFont("radioSurfaceXs", {
|
||||
font = "Consolas",
|
||||
size = 9,
|
||||
extended = true,
|
||||
weight = 400,
|
||||
antialias = true,
|
||||
})
|
||||
|
||||
surface.CreateFont("radioSurfaceSm", {
|
||||
font = "Consolas",
|
||||
size = 12,
|
||||
extended = true,
|
||||
weight = 400,
|
||||
antialias = true,
|
||||
})
|
||||
|
||||
surface.CreateFont("radioSurfaceSmLight", {
|
||||
font = "Open Sans Bold",
|
||||
size = 18,
|
||||
extended = true,
|
||||
weight = 200,
|
||||
antialias = true,
|
||||
})
|
||||
|
||||
surface.CreateFont("radioSurfaceReg", {
|
||||
font = "Consolas",
|
||||
size = 16,
|
||||
extended = true,
|
||||
weight = 400,
|
||||
antialias = true,
|
||||
})
|
||||
|
||||
surface.CreateFont("radioSurfaceBold", {
|
||||
font = "Consolas",
|
||||
extended = true,
|
||||
size = 20,
|
||||
weight = 800,
|
||||
antialias = true,
|
||||
})
|
||||
|
||||
timer.Create("UpdateMusicRadioDisplayData", 0.15, 0, function()
|
||||
for _, ent in ipairs(ents.FindByClass("wn_musicradio")) do
|
||||
if (ent and ent.IsPlayingMusic) then
|
||||
updateMRadioSigLine(ent)
|
||||
updateMRadioFreqDialLine(ent)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
hook.Add("PostDrawOpaqueRenderables", "draw_music_radio_ui", function()
|
||||
local epos
|
||||
local eang
|
||||
local lpos
|
||||
local lang
|
||||
local scale = 0.05 -- dat nice scale for good text (still not enough)
|
||||
local distsqr = 500 ^ 2
|
||||
local client = LocalPlayer()
|
||||
|
||||
for _, ent in ipairs(ents.FindByClass("wn_musicradio")) do
|
||||
if (client:GetPos():DistToSqr(ent:GetPos()) < distsqr) then
|
||||
if (!ent or !IsEntity(ent)) then
|
||||
continue
|
||||
end
|
||||
|
||||
epos = ent:GetPos()
|
||||
eang = ent:GetAngles()
|
||||
|
||||
if (!epos or !isvector(epos) or !eang or !isangle(eang)) then
|
||||
continue
|
||||
end
|
||||
|
||||
--[[
|
||||
Set the pos and angle so that they match the face of the radio prop
|
||||
And so that they align to the top left corner
|
||||
And are local to the entity in worldspace
|
||||
]]
|
||||
lpos = ent:LocalToWorld(Vector(PLUGIN.x, PLUGIN.y, PLUGIN.z))
|
||||
lang = ent:LocalToWorldAngles(Angle(PLUGIN.pitch, PLUGIN.yaw, PLUGIN.roll))
|
||||
cam.Start3D2D(lpos, lang, scale)
|
||||
drawMRadioSurface(ent, lpos, lang, scale)
|
||||
cam.End3D2D()
|
||||
-- do the same thing, but a bit further forward, for the hitbox rectangles
|
||||
lpos2 = ent:LocalToWorld(Vector(PLUGIN.x + 0.9, PLUGIN.y, PLUGIN.z))
|
||||
cam.Start3D2D(lpos2, lang, scale)
|
||||
drawActiveHitbox()
|
||||
cam.End3D2D()
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--[[
|
||||
this code is sus
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣤⣤⣤⣤⣤⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⣿⡿⠛⠉⠙⠛⠛⠛⠛⠻⢿⣿⣷⣤⡀⠀⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⠋⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⠈⢻⣿⣿⡄⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⣸⣿⡏⠀⠀⠀⣠⣶⣾⣿⣿⣿⠿⠿⠿⢿⣿⣿⣿⣄⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⠁⠀⠀⢰⣿⣿⣯⠁⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣷⡄⠀
|
||||
⠀⠀⣀⣤⣴⣶⣶⣿⡟⠀⠀⠀⢸⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣷⠀
|
||||
⠀⢰⣿⡟⠋⠉⣹⣿⡇⠀⠀⠀⠘⣿⣿⣿⣿⣷⣦⣤⣤⣤⣶⣶⣶⣶⣿⣿⣿⠀
|
||||
⠀⢸⣿⡇⠀⠀⣿⣿⡇⠀⠀⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠃⠀
|
||||
⠀⣸⣿⡇⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠉⠻⠿⣿⣿⣿⣿⡿⠿⠿⠛⢻⣿⡇⠀⠀
|
||||
⠀⣿⣿⠁⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣧⠀⠀
|
||||
⠀⣿⣿⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠀⠀
|
||||
⠀⣿⣿⠀⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣿⠀⠀
|
||||
⠀⢿⣿⡆⠀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⡇⠀⠀
|
||||
⠀⠸⣿⣧⡀⠀⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⠃⠀⠀
|
||||
⠀⠀⠛⢿⣿⣿⣿⣿⣇⠀⠀⠀⠀⠀⣰⣿⣿⣷⣶⣶⣶⣶⠶⠀⢠⣿⣿⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⣿⣿⡇⠀⣽⣿⡏⠁⠀⠀⢸⣿⡇⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⣿⣿⠀⠀⠀⠀⠀⣿⣿⡇⠀⢹⣿⡆⠀⠀⠀⣸⣿⠇⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⢿⣿⣦⣄⣀⣠⣴⣿⣿⠁⠀⠈⠻⣿⣿⣿⣿⡿⠏⠀⠀⠀⠀
|
||||
⠀⠀⠀⠀⠀⠀⠀⠈⠛⠻⠿⠿⠿⠿⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
|
||||
]]
|
||||
function PLUGIN:Think()
|
||||
-- we're checking every frame if the player is looking at a radio??
|
||||
-- someone cooked here
|
||||
local tr = LocalPlayer():GetEyeTrace()
|
||||
if (tr.Entity and IsValid(tr.Entity) and tr.Entity:GetClass() == "wn_musicradio") then
|
||||
local volButton = tr.Entity:GetAbsolutePanelButtonPosition(80, 170)
|
||||
local onButton = tr.Entity:GetAbsolutePanelButtonPosition(135, 150)
|
||||
local dialButton = tr.Entity:GetAbsolutePanelButtonPosition(190, 170)
|
||||
|
||||
hitboxTargets.volume.shouldDraw = false
|
||||
hitboxTargets.onOff.shouldDraw = false
|
||||
hitboxTargets.dial.shouldDraw = false
|
||||
|
||||
if (tr.HitPos:Distance(volButton) <= 2) then
|
||||
if (tr.Entity:IsPlayingMusic()) then
|
||||
hitboxTargets.volume.shouldDraw = true
|
||||
end
|
||||
elseif (tr.HitPos:Distance(onButton) <= 2) then
|
||||
hitboxTargets.onOff.shouldDraw = true
|
||||
elseif (tr.HitPos:Distance(dialButton) <= 2) then
|
||||
if (tr.Entity:IsPlayingMusic()) then
|
||||
hitboxTargets.dial.shouldDraw = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,338 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
ENT.Type = "anim"
|
||||
ENT.Base = "base_gmodentity"
|
||||
|
||||
ENT.PrintName = "Music Radio"
|
||||
ENT.Author = "M!NT"
|
||||
ENT.Category = "HL2 RP"
|
||||
ENT.Contact = ""
|
||||
ENT.Purpose = ""
|
||||
ENT.Instructions = ""
|
||||
ENT.Spawnable = false
|
||||
ENT.AdminOnly = true
|
||||
ENT.Holdable = true
|
||||
|
||||
function ENT:SetupDataTables()
|
||||
self:NetworkVar("Bool", 0, "Enabled")
|
||||
end
|
||||
|
||||
function ENT:IsPlayingMusic()
|
||||
return self:GetNWString("curChan", "") != ""
|
||||
end
|
||||
|
||||
function ENT:GetChan()
|
||||
return self:GetNWString("curChan", "")
|
||||
end
|
||||
|
||||
function ENT:GetVolume()
|
||||
return self:GetNWInt("vol", 1)
|
||||
end
|
||||
|
||||
function ENT:GetLevel()
|
||||
return self:GetNWInt("db", 70)
|
||||
end
|
||||
|
||||
if (CLIENT) then
|
||||
function ENT:GetIlluminatedVolButton()
|
||||
-- volume is a value of 0 - 1. converts that to 1 - 10 (active illum button)
|
||||
return math.Round(10 * self:GetNWInt("vol", 1), 0)
|
||||
end
|
||||
|
||||
-- volume from 1 - 6
|
||||
function ENT:SetVolume(vol)
|
||||
if (vol > 10) then
|
||||
vol = 1
|
||||
end
|
||||
|
||||
-- normalize vol between 0 and 1
|
||||
local normal = vol / 10
|
||||
-- normalize vol between maxdb (72) and mindb (57)
|
||||
local newLvl = math.ceil(57 + (vol - 1) * (72 - 57) / (10 - 1))
|
||||
|
||||
net.Start("SetMusicRadioVolume")
|
||||
net.WriteEntity(self)
|
||||
net.WriteFloat(normal)
|
||||
net.WriteInt(newLvl, 8)
|
||||
net.SendToServer()
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:onTakeDmg()
|
||||
ix.musicRadio:PlayTuneStatic(self, true)
|
||||
end
|
||||
|
||||
function ENT:PhysicsCollide(data, phys)
|
||||
if (!self:IsPlayingMusic()) then
|
||||
return
|
||||
end
|
||||
|
||||
if (data.Speed < 125) then
|
||||
return
|
||||
end
|
||||
|
||||
local curTime = CurTime()
|
||||
|
||||
if (self.nextCollide and self.nextCollide > curTime) then
|
||||
return
|
||||
end
|
||||
|
||||
self:onTakeDmg()
|
||||
self.nextCollide = curTime + 3
|
||||
end
|
||||
|
||||
function ENT:OnTakeDamage(dmginfo)
|
||||
if (!self:IsPlayingMusic()) then
|
||||
return
|
||||
end
|
||||
|
||||
if (dmginfo:GetDamage() < 5) then
|
||||
return
|
||||
end
|
||||
|
||||
if (dmginfo:GetDamage() > 20) then
|
||||
self:DoBreak(dmginfo)
|
||||
return
|
||||
end
|
||||
|
||||
local curTime = CurTime()
|
||||
|
||||
if (self.nextDmg and self.nextDmg > curTime) then
|
||||
return
|
||||
end
|
||||
|
||||
self:onTakeDmg()
|
||||
self.nextDmg = curTime + 3
|
||||
end
|
||||
|
||||
function ENT:DoBreak(dmgInfo)
|
||||
self:StopCurrentSong(0)
|
||||
|
||||
local explode = ents.Create("env_explosion")
|
||||
explode:SetPos(self:GetPos())
|
||||
explode:SetOwner(dmgInfo:GetAttacker())
|
||||
explode:Spawn()
|
||||
explode:SetKeyValue("iMagnitude", "10")
|
||||
explode:Fire("Explode", 0, 0)
|
||||
explode:EmitSound("Glass.Break", 120, 90)
|
||||
|
||||
local gib = ents.Create("prop_physics")
|
||||
gib:SetModel("models/props_lab/citizenradio.mdl")
|
||||
gib:SetPos(self:GetPos())
|
||||
gib:Spawn()
|
||||
gib:Ignite(3, 100) -- ignite for 3s, ignite everything within 100hus
|
||||
gib:EmitSound("willardnetworks/musicradio/musicradio_static_loopable.mp3", 110, 85)
|
||||
|
||||
self:Remove()
|
||||
end
|
||||
|
||||
function ENT:SetChan(chan)
|
||||
if (CLIENT) then
|
||||
net.Start("SetMusicRadioChannel")
|
||||
net.WriteEntity(self)
|
||||
net.WriteString(chan)
|
||||
net.SendToServer()
|
||||
end
|
||||
|
||||
if (SERVER) then
|
||||
if (self:IsPlayingMusic()) then
|
||||
ix.musicRadio:TuneOut(self, self:GetChan(), chan)
|
||||
end
|
||||
|
||||
ix.musicRadio:TuneIn(self, chan)
|
||||
self:SetNWString("curChan", chan)
|
||||
end
|
||||
end
|
||||
|
||||
function ENT:GetAbsolutePanelButtonPosition(x, y)
|
||||
local absPos = self:GetPos()
|
||||
absPos = absPos + self:GetForward() * 10
|
||||
absPos = absPos + self:GetRight() * (17 + (1 - (x * 0.1)))
|
||||
absPos = absPos + self:GetUp() * (24 + (1 - (y * 0.1)))
|
||||
return absPos
|
||||
end
|
||||
|
||||
if (SERVER) then
|
||||
util.AddNetworkString("SetMusicRadioChannel")
|
||||
util.AddNetworkString("SetMusicRadioVolume")
|
||||
|
||||
net.Receive("SetMusicRadioChannel", function(len, ply)
|
||||
local ent = net.ReadEntity()
|
||||
local ch = net.ReadString()
|
||||
|
||||
ent:SetChan(ch)
|
||||
end)
|
||||
|
||||
net.Receive("SetMusicRadioVolume", function(len, ply)
|
||||
local ent = net.ReadEntity()
|
||||
local vol = net.ReadString()
|
||||
local db = net.ReadInt(8)
|
||||
|
||||
ent:SetVolume(vol, db)
|
||||
end)
|
||||
|
||||
-- handle the interaction with the volume knob
|
||||
function ENT:InteractVolumeKnob()
|
||||
local vol = math.ceil(self:GetVolume() * 10)
|
||||
if (vol >= 10) then
|
||||
vol = 1
|
||||
else
|
||||
vol = vol + 1
|
||||
end
|
||||
|
||||
-- normalize vol between 0 and 1
|
||||
local newVol = vol / 10
|
||||
-- normalize vol between maxdb (65) and mindb (55)
|
||||
local newLvl = math.ceil(50 + (vol - 1) * (65 - 50) / (10 - 1))
|
||||
|
||||
self:SetVolume(newVol, newLvl)
|
||||
|
||||
-- play clicky noise for button
|
||||
self:EmitSound("willardnetworks/musicradio/musicradio_click_"..tostring(math.random(1, 4))..".mp3", 70)
|
||||
end
|
||||
|
||||
-- 0 - 1, 60 - 85
|
||||
function ENT:SetVolume(vol, db, delta, bStop)
|
||||
local ch = self:GetChan()
|
||||
local curSong = ix.musicRadio.destinations[ch].curSong
|
||||
if (!delta) then
|
||||
delta = 1
|
||||
end
|
||||
|
||||
ix.musicRadio.destinations[ch][self:EntIndex()].soundCache[curSong]
|
||||
:ChangeVolume(vol, delta)
|
||||
|
||||
ix.musicRadio.destinations[ch][self:EntIndex()].soundCache[curSong]
|
||||
:SetSoundLevel(db)
|
||||
|
||||
if (bStop) then
|
||||
ix.musicRadio.destinations[ch][self:EntIndex()].soundCache[curSong]:Stop()
|
||||
end
|
||||
|
||||
self:SetNWInt("vol", vol)
|
||||
self:SetNWInt("db", db)
|
||||
end
|
||||
|
||||
function ENT:StopCurrentSong(delta)
|
||||
local ch = self:GetChan()
|
||||
local curSong = ix.musicRadio.destinations[ch].curSong
|
||||
|
||||
if (!ch or !curSong) then
|
||||
return
|
||||
end
|
||||
if (!delta) then
|
||||
delta = 0.5
|
||||
end
|
||||
|
||||
if (!ix.musicRadio.destinations[ch][self:EntIndex()]) then
|
||||
return -- ??
|
||||
end
|
||||
|
||||
ix.musicRadio.destinations[ch][self:EntIndex()].soundCache[curSong]:FadeOut(delta)
|
||||
end
|
||||
|
||||
function ENT:Initialize()
|
||||
self:SetModel("models/props_lab/citizenradio.mdl")
|
||||
self:PhysicsInit(SOLID_VPHYSICS)
|
||||
self:SetMoveType(MOVETYPE_VPHYSICS)
|
||||
self:SetSolid(SOLID_VPHYSICS)
|
||||
|
||||
local phys = self:GetPhysicsObject()
|
||||
if IsValid(phys) then
|
||||
phys:EnableMotion(true)
|
||||
phys:Wake()
|
||||
end
|
||||
|
||||
self:SetRadioClass("benefactor")
|
||||
end
|
||||
|
||||
function ENT:SetRadioClass(className)
|
||||
self.defaultStation = ix.musicRadio:GetDefaultChannel(className)
|
||||
end
|
||||
|
||||
function ENT:GetRadioClass()
|
||||
return ix.musicRadio.channels[self.defaultStation].class
|
||||
end
|
||||
|
||||
function ENT:PerformPickup(client)
|
||||
if timer.Exists("ixCharacterInteraction" .. client:SteamID()) then return end
|
||||
|
||||
local itemClassName = "musicradio_cmb"
|
||||
if (self:GetRadioClass() == "pirate") then
|
||||
itemClassName = "musicradio_reb"
|
||||
end
|
||||
|
||||
client:PerformInteraction(0.5, self, function(_)
|
||||
local success = client:GetCharacter():GetInventory():Add(itemClassName)
|
||||
if (!success or success == false) then
|
||||
client:Notify("There is not enough room in your inventory!")
|
||||
return false
|
||||
end
|
||||
|
||||
self:Remove()
|
||||
timer.Remove("ixCharacterInteraction" .. client:SteamID())
|
||||
end)
|
||||
end
|
||||
|
||||
function ENT:Use(activator)
|
||||
local curTime = CurTime()
|
||||
|
||||
if (self.nextUse and self.nextUse > curTime) then
|
||||
return
|
||||
end
|
||||
|
||||
if activator:KeyDown(IN_WALK) then
|
||||
return self:PerformPickup(activator)
|
||||
end
|
||||
|
||||
-- yay! magic numbers!!
|
||||
-- these loosely fit the derma buttons, which is why we're doing it this way
|
||||
local volButton = self:GetAbsolutePanelButtonPosition(80, 170)
|
||||
local onButton = self:GetAbsolutePanelButtonPosition(135, 150)
|
||||
local dialButton = self:GetAbsolutePanelButtonPosition(190, 170)
|
||||
|
||||
local tr = activator:GetEyeTrace()
|
||||
if (tr.HitPos:Distance(volButton) <= 4) then
|
||||
if (self:IsPlayingMusic()) then
|
||||
self:InteractVolumeKnob()
|
||||
end
|
||||
|
||||
elseif (tr.HitPos:Distance(onButton) <= 4) then
|
||||
self:EmitSound("willardnetworks/musicradio/musicradio_click_"..tostring(math.random(1, 4))..".mp3", 70)
|
||||
|
||||
if (self:IsPlayingMusic()) then
|
||||
self:StopCurrentSong()
|
||||
ix.musicRadio:TuneOut(self, self:GetChan())
|
||||
self:SetNWString("curChan", "")
|
||||
else
|
||||
self:SetNWString("curChan", self.defaultStation)
|
||||
ix.musicRadio:TuneIn(self, self.defaultStation)
|
||||
end
|
||||
|
||||
elseif (tr.HitPos:Distance(dialButton) <= 4) then
|
||||
if (self.nextUse and (self.nextUse + 2) > curTime) then
|
||||
return
|
||||
end
|
||||
|
||||
if (self:IsPlayingMusic()) then
|
||||
-- only allow channel changes when the radio is on
|
||||
self:SetChan(ix.musicRadio:GetNextChannelName(
|
||||
self:GetNWString("curChan", self.defaultStation)))
|
||||
end
|
||||
end
|
||||
|
||||
self.nextUse = curTime + 1
|
||||
end
|
||||
else
|
||||
function ENT:Think()
|
||||
self.ShowPlayerInteraction = LocalPlayer():KeyDown(IN_WALK)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,48 @@
|
||||
--[[
|
||||
| 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
|
||||
|
||||
ITEM.name = "Benefactor Radio"
|
||||
ITEM.model = Model("models/props_lab/citizenradio.mdl")
|
||||
ITEM.description = "A manufactured radio with a tuner that can only tune to official Union-Approved radio stations."
|
||||
ITEM.width = 4
|
||||
ITEM.height = 3
|
||||
ITEM.category = "Technology"
|
||||
ITEM.functions.Deploy = {
|
||||
name = "Deploy",
|
||||
OnRun = function(itemTable)
|
||||
local client = itemTable.player
|
||||
if client.CantPlace then
|
||||
client:NotifyLocalized("You need to wait before you can place this!..")
|
||||
return false
|
||||
end
|
||||
|
||||
client.CantPlace = true
|
||||
|
||||
timer.Simple(3, function()
|
||||
if client then
|
||||
client.CantPlace = false
|
||||
end
|
||||
end)
|
||||
|
||||
local radio = ents.Create("wn_musicradio")
|
||||
local tr = client:GetEyeTrace()
|
||||
local dist = client:EyePos():Distance(tr.HitPos)
|
||||
|
||||
radio:SetPos(client:EyePos() + (tr.Normal * math.Clamp(dist, 0, 75)))
|
||||
radio:Spawn()
|
||||
radio:SetRadioClass("benefactor")
|
||||
|
||||
ix.saveEnts:SaveEntity(radio)
|
||||
|
||||
return true
|
||||
end
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
--[[
|
||||
| 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
|
||||
|
||||
ITEM.name = "Rebel Radio"
|
||||
ITEM.model = Model("models/props_lab/citizenradio.mdl")
|
||||
ITEM.description = "A small, salvaged radio with a tuner that can be tuned to anti-combine pirated radio stations."
|
||||
ITEM.category = "Technology"
|
||||
ITEM.width = 4
|
||||
ITEM.height = 3
|
||||
ITEM.functions.Deploy = {
|
||||
name = "Deploy",
|
||||
OnRun = function(itemTable)
|
||||
local client = itemTable.player
|
||||
if client.CantPlace then
|
||||
client:NotifyLocalized("You need to wait before you can place this!..")
|
||||
return false
|
||||
end
|
||||
|
||||
client.CantPlace = true
|
||||
|
||||
timer.Simple(3, function()
|
||||
if client then
|
||||
client.CantPlace = false
|
||||
end
|
||||
end)
|
||||
|
||||
local radio = ents.Create("wn_musicradio")
|
||||
local tr = client:GetEyeTrace()
|
||||
local dist = client:EyePos():Distance(tr.HitPos)
|
||||
|
||||
radio:SetPos(client:EyePos() + (tr.Normal * math.Clamp(dist, 0, 75)))
|
||||
radio:Spawn()
|
||||
radio:SetRadioClass("pirate")
|
||||
|
||||
ix.saveEnts:SaveEntity(radio)
|
||||
|
||||
return true
|
||||
end
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
ITEM.name = "Combine Approved Radio Tuner"
|
||||
ITEM.description = "A radio tuner which can be used to modify the frequencies a radio can tune to."
|
||||
ITEM.category = "Technology"
|
||||
ITEM.width = 1
|
||||
ITEM.height = 1
|
||||
ITEM.model = "models/willardnetworks/skills/circuit.mdl"
|
||||
ITEM.colorAppendix = {["blue"] = "You can acquire this item via the Crafting skill."}
|
||||
ITEM.maxStackSize = 1
|
||||
ITEM.functions.install = {
|
||||
name = "Install",
|
||||
tip = "Install this item into the radio you're looking at.",
|
||||
icon = "icon16/wrench.png",
|
||||
OnRun = function(item)
|
||||
ix.musicRadio:InstallTuner(item.player, false)
|
||||
end
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
--[[
|
||||
| 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/
|
||||
--]]
|
||||
|
||||
ITEM.name = "Resistance Radio Tuner"
|
||||
ITEM.description = "A radio tuner which can be used to modify the frequencies a radio can pick up."
|
||||
ITEM.category = "Technology"
|
||||
ITEM.width = 1
|
||||
ITEM.height = 1
|
||||
ITEM.model = "models/willardnetworks/skills/circuit.mdl"
|
||||
ITEM.colorAppendix = {["blue"] = "You can acquire this item via the Crafting skill.", ["red"] = "It is suspicious to carry this item."}
|
||||
ITEM.maxStackSize = 1
|
||||
ITEM.functions.install = {
|
||||
name = "Install",
|
||||
tip = "Install this item into the radio you're looking at.",
|
||||
icon = "icon16/wrench.png",
|
||||
OnRun = function(item)
|
||||
ix.musicRadio:InstallTuner(item.player, true)
|
||||
end
|
||||
}
|
||||
633
gamemodes/ixhl2rp/plugins/better_music_radio/sh_plugin.lua
Normal file
633
gamemodes/ixhl2rp/plugins/better_music_radio/sh_plugin.lua
Normal file
@@ -0,0 +1,633 @@
|
||||
--[[
|
||||
| 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
|
||||
PLUGIN.name = "Better Music Radio"
|
||||
PLUGIN.author = "M!NT"
|
||||
PLUGIN.description = "Adds a radio that plays music, but better c:"
|
||||
|
||||
ix.util.Include("sv_hooks.lua")
|
||||
ix.util.Include("cl_hooks.lua")
|
||||
|
||||
ix.musicRadio = ix.musicRadio or {}
|
||||
ix.musicRadio.announcements = ix.musicRadio.annnouncements or {}
|
||||
ix.musicRadio.channels = ix.musicRadio.channels or {}
|
||||
ix.musicRadio.classes = ix.musicRadio.classes or {
|
||||
["pirate"] = "wn_rebel_radio",
|
||||
["benefactor"] = "wn_cmb_radio"
|
||||
}
|
||||
ix.musicRadio.static = ix.musicRadio.static or {}
|
||||
ix.musicRadio.classes = ix.musicRadio.classes or {}
|
||||
-- enums
|
||||
ix.musicRadio.CHAN_DISABLED = true
|
||||
ix.musicRadio.CHAN_ENABLED = false
|
||||
|
||||
CAMI.RegisterPrivilege({
|
||||
Name = "Helix - Manage Music Radios",
|
||||
MinAccess = "admin"
|
||||
})
|
||||
|
||||
ix.command.Add("MusicRadioChannelSeek", {
|
||||
description = "Seek the specific music radio channel forward one song.",
|
||||
privilege = "Manage Music Radios",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string
|
||||
},
|
||||
OnRun = function(self, client, channelName)
|
||||
if (!ix.musicRadio:ChannelIsValid(channelName)) then
|
||||
client:Notify("Invalid channel name provided!")
|
||||
else
|
||||
ix.musicRadio:PlayNextSong(channelName)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("PlaySoundOnRadioClass", {
|
||||
description = "Play a specific song on all radios in a certain class on the map.",
|
||||
privilege = "Manage Music Radios",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string,
|
||||
ix.type.string,
|
||||
ix.type.number
|
||||
},
|
||||
OnRun = function(self, client, class, soundName, length)
|
||||
ix.musicRadio:PlaySoundOnClass(soundName, class, length)
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("SetMusicRadioClassEnabled", {
|
||||
description = "Disable a specific class of music radios.",
|
||||
privilege = "Manage Music Radios",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string
|
||||
},
|
||||
OnRun = function(self, client, class)
|
||||
if (!ix.musicRadio:ClassIsValid(class)) then
|
||||
client:Notify("Invalid class name provided!")
|
||||
else
|
||||
if (!ix.musicRadio:GetClassShouldPlayStatic(class)) then
|
||||
client:Notify("This class is currently enabled. Use the 'SetMusicRadioClassDisabled' command to disable it.")
|
||||
else
|
||||
ix.musicRadio:SetClassStaticState(class, ix.musicRadio.CHAN_ENABLED)
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("SetMusicRadioClassDisabled", {
|
||||
description = "Enable a specific class of music radios.",
|
||||
privilege = "Manage Music Radios",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string
|
||||
},
|
||||
OnRun = function(self, client, class)
|
||||
if (!ix.musicRadio:ClassIsValid(class)) then
|
||||
client:Notify("Invalid class name provided!")
|
||||
else
|
||||
if (ix.musicRadio:GetClassShouldPlayStatic(class)) then
|
||||
client:Notify("This class is currently disabled. Use the 'SetMusicRadioClassEnabled' command to enable it.")
|
||||
else
|
||||
ix.musicRadio:SetClassStaticState(class, ix.musicRadio.CHAN_DISABLED)
|
||||
end
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("RestartMusicRadioClass", {
|
||||
description = "Restart a specific class of music radios (in case they crash for some reason).",
|
||||
privilege = "Manage Music Radios",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string
|
||||
},
|
||||
OnRun = function(self, client, class)
|
||||
if (!ix.musicRadio:ClassIsValid(class)) then
|
||||
client:Notify("Invalid class name provided!")
|
||||
else
|
||||
ix.musicRadio:RestartClass(class)
|
||||
client:Notify("Restarted class: "..class)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
ix.command.Add("RestartMusicRadioChannel", {
|
||||
description = "Restart a specific radio channel (in case it crashes for some reason).",
|
||||
privilege = "Manage Music Radios",
|
||||
adminOnly = true,
|
||||
arguments = {
|
||||
ix.type.string
|
||||
},
|
||||
OnRun = function(self, client, channel)
|
||||
if (!ix.musicRadio:ChannelIsValid(channel)) then
|
||||
client:Notify("Invalid channel name provided!")
|
||||
else
|
||||
ix.musicRadio:RestartChannel(channel)
|
||||
client:Notify("Restarted channel: "..channel)
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
function ix.musicRadio:InitStatic()
|
||||
--[[
|
||||
STATIC sounds for when the channel / class is KOd
|
||||
]]
|
||||
local static = {
|
||||
[1] = { fname = "willardnetworks/musicradio/static/musicradio_static_1.mp3", length = 55 },
|
||||
[2] = { fname = "willardnetworks/musicradio/static/musicradio_static_2.mp3", length = 55 },
|
||||
[3] = { fname = "willardnetworks/musicradio/static/musicradio_static_3.mp3", length = 55 }
|
||||
}
|
||||
|
||||
self.static.sounds = static
|
||||
end
|
||||
|
||||
|
||||
function ix.musicRadio:InitSpooky()
|
||||
--[[
|
||||
Spooky sounds for fun
|
||||
]]
|
||||
local classes = {
|
||||
["pirate"] = true,
|
||||
["benefactor"] = false
|
||||
}
|
||||
|
||||
local sounds = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_1.mp3", length = 170 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_2.mp3", length = 170 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_3.mp3", length = 52 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_4.mp3", length = 11 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_5.mp3", length = 9 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_6.mp3", length = 12 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/spooky/musicradio_spooky_7.mp3", length = 60 },
|
||||
}
|
||||
|
||||
self.spooky = {}
|
||||
self.spooky.sounds = sounds
|
||||
self.spooky.classes = classes
|
||||
end
|
||||
|
||||
function ix.musicRadio:PrecacheSpooky()
|
||||
if (SERVER) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!istable(self.spooky)) then
|
||||
return
|
||||
end
|
||||
|
||||
for k, song in ipairs(self.spooky.sounds) do
|
||||
util.PrecacheSound(song.fname)
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:AddAnnouncementsToClass(className, announcerTbl)
|
||||
if (!self.classes[className]) then
|
||||
-- happens a lot during lua refresh
|
||||
-- if that happens to you, safe to ignore
|
||||
-- just keeping this here in case an announcer class doesn't come up during refresh
|
||||
ErrorNoHaltWithStack("Attempt to add invalid announcer! Not adding.. Class: "..tostring(className))
|
||||
return
|
||||
else
|
||||
if (!self.announcements) then
|
||||
self.announcements = {}
|
||||
end
|
||||
|
||||
self.announcements[className] = {}
|
||||
self.announcements[className].sounds = announcerTbl
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:PrecacheAnnouncementsForClass(className)
|
||||
if (SERVER) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!istable(self.classes[className])) then
|
||||
return
|
||||
end
|
||||
|
||||
for k, snd in ipairs(self.announcements[className]) do
|
||||
util.PrecacheSound(snd.fname)
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:PrecacheChannel(chName)
|
||||
if (SERVER) then
|
||||
return
|
||||
end
|
||||
|
||||
if (!istable(self.channels[chName])) then
|
||||
return
|
||||
end
|
||||
|
||||
for k, song in ipairs(self.channels[chName]) do
|
||||
util.PrecacheSound(song.fname)
|
||||
end
|
||||
end
|
||||
|
||||
-- get the 'next' channel; seek forward
|
||||
function ix.musicRadio:GetNextChannelName(chName)
|
||||
local class = self.channels[chName].class
|
||||
if (!chName or chName == "") then
|
||||
return self.chanList[class][1]
|
||||
end
|
||||
|
||||
local k = table.KeyFromValue(self.chanList[class], chName)
|
||||
|
||||
if (k and isnumber(k)) then
|
||||
if (k == table.Count(self.chanList[class])) then
|
||||
return self.chanList[class][1] -- loop around
|
||||
else
|
||||
return self.chanList[class][k+1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:GetDefaultChannel(className)
|
||||
if (!self.classes[className]) then
|
||||
ErrorNoHaltWithStack("Attempt to add radio to invalid channel! Not adding.. Class: "..tostring(className))
|
||||
return
|
||||
end
|
||||
|
||||
return ix.musicRadio.chanList[className][1]
|
||||
end
|
||||
|
||||
function ix.musicRadio:AddChannel(chName, mTbl, class, freqMap)
|
||||
if (!self.classes[class]) then
|
||||
-- happens a lot during lua refresh
|
||||
-- if that happens to you, safe to ignore
|
||||
-- just keeping this here in case a channel doesn't come up during refresh
|
||||
ErrorNoHaltWithStack("Attempt to add invalid channel! Not adding.. Name: "..tostring(chName).." Class: "..tostring(class))
|
||||
return
|
||||
else
|
||||
self.channels[chName] = {}
|
||||
self.channels[chName].songs = mTbl
|
||||
self.channels[chName].freqMap = freqMap -- used clientside to figure out where the dial goes
|
||||
self.channels[chName].class = class
|
||||
|
||||
if (!self.chanList) then
|
||||
self.chanList = {}
|
||||
end
|
||||
|
||||
if (!self.chanList[class]) then
|
||||
self.chanList[class] = {}
|
||||
end
|
||||
|
||||
table.insert(self.chanList[class], chName)
|
||||
end
|
||||
end
|
||||
|
||||
function ix.musicRadio:GetChannel(chName)
|
||||
if (self.channels[chName] and istable(self.channels[chName])) then
|
||||
return self.channels[chName]
|
||||
end
|
||||
end
|
||||
|
||||
--[[
|
||||
RADIO SETUP
|
||||
]]
|
||||
do
|
||||
-- load up the current state of static
|
||||
ix.musicRadio:InitStatic()
|
||||
|
||||
-- load up some stuff for spooks
|
||||
ix.musicRadio:InitSpooky()
|
||||
ix.musicRadio:PrecacheSpooky()
|
||||
|
||||
-- the static blurb that plays during tuning
|
||||
local staticSoundFilePrefix = "willardnetworks/musicradio/musicradio_static_"
|
||||
for i=1, 6 do
|
||||
util.PrecacheSound(staticSoundFilePrefix..tostring(i)..".mp3")
|
||||
end
|
||||
|
||||
--[[
|
||||
REBEL RADIO STATIONS
|
||||
]]
|
||||
local bluesRock = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_acdc_jailbreak.mp3", length = 275 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_aerosmith_dreamon.mp3", length = 260 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_allmanbrothers_midnightrider.mp3", length = 173 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_allmanbrothers_whippingpost.mp3", length = 316 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_bbking_thrillisgone.mp3", length = 309 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_beatles_taxman.mp3", length = 151 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_billwithers_aintnosunshine.mp3", length = 118 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_cream_strangebrew.mp3", length = 164 },
|
||||
[9] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_dirtymac_yerblues.mp3", length = 242 },
|
||||
[10] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_jacksonbrowne_doctormyeyes.mp3", length = 186 },
|
||||
[11] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_jimihendrix_allalongthewatchtower.mp3", length = 238 },
|
||||
[12] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_jimihendrix_littlewing.mp3", length = 138 },
|
||||
[13] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_jimihendrix_purplehaze.mp3", length = 164 },
|
||||
[14] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_johnleehooker_boomboomboomboom.mp3", length = 156 },
|
||||
[15] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_ledzepplin_rambleon.mp3", length = 240 },
|
||||
[16] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_ledzepplin_stairwaytoheaven.mp3", length = 467 },
|
||||
[17] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_lynyrdskynyrd_amilosin.mp3", length = 251 },
|
||||
[18] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_lynyrdskynyrd_cryforthebadman.mp3", length = 280 },
|
||||
[19] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_lynyrdskynyrd_freebird.mp3", length = 518 },
|
||||
[20] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_lynyrdskynyrd_onthehunt.mp3", length = 315 },
|
||||
[21] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_lynyrdskynyrd_saturdaynightspecial.mp3", length = 291 },
|
||||
[22] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_srv_prideandjoy.mp3", length = 216 },
|
||||
[23] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_srv_texasflood.mp3", length = 317 },
|
||||
[24] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_srv_voodoochild.mp3", length = 455 },
|
||||
[25] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_stonetemplepilots_interstatelovesong.mp3", length = 187 },
|
||||
[26] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_tednugent_stranglehold.mp3", length = 500 },
|
||||
[27] = { fname = "willardnetworks/musicradio/rebel/bluesrock/musicradio_theband_thenighttheydroveolddixiedown.mp3", length = 247 }
|
||||
}
|
||||
|
||||
local hipHop = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_big_hypnotize.mp3", length = 225 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_fugees_zealots.mp3", length = 243 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_getoboys_damnitfeelsgoodtobeagangasta.mp3", length = 298 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_getoboys_still.mp3", length = 216 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_heltahskeltah_lethabrainzblo.mp3", length = 243 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_jamoroquai_virtualinstanity.mp3", length = 223 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_mfdoom_doomsday.mp3", length = 247 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_nas_nystateofmind.mp3", length = 288 },
|
||||
[9] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_nwa_fuckdapolice.mp3", length = 313 },
|
||||
[10] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_outkast_atliens.mp3", length = 208 },
|
||||
[11] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_outkast_elevators.mp3", length = 234 },
|
||||
[12] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_peterock_troy.mp3", length = 273 },
|
||||
[13] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_puts_acidraindrops.mp3", length = 264 },
|
||||
[14] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_sugarhillgang_rappersdelight.mp3", length = 236 },
|
||||
[15] = { fname = "willardnetworks/musicradio/rebel/hiphop/musicradio_wutang_cream.mp3", length = 235 }
|
||||
}
|
||||
|
||||
local country = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_alabama_dixielanddelight.mp3", length = 233 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_billmonroe_bluemoonofkentucky.mp3", length = 186 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_charliedaniels_southsgondoitagain.mp3", length = 235 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_docwatson_houseoftherisingsun.mp3", length = 193 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_gordonlightfoot_edmundfitzgerald.mp3", length = 351 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_jimmymartin_freebornman.mp3", length = 169 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_jimmymartin_madeintheshade.mp3", length = 181 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_keithwhitley_miamimyami.mp3", length = 201 },
|
||||
[9] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_oldcrow_takeemaway.mp3", length = 208 },
|
||||
[10] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_oldcrow_wagonwheel.mp3", legnth = 227 },
|
||||
[11] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_osbornebrothers_rockytop.mp3", legnth = 151 },
|
||||
[12] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_osbornebrothers_sunnysideofthemountain.mp3", legnth = 138 },
|
||||
[13] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_ralphstanely_clinchmountainbackstep.mp3", legnth = 136 },
|
||||
[14] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_ralphstanely_eastvirginiablues.mp3", legnth = 151 },
|
||||
[15] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_ralphstanely_gloryland.mp3", legnth = 160 },
|
||||
[16] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_ralphstanely_manofconstantsorrow.mp3", legnth = 165 },
|
||||
[17] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_ralphstanely_rankstranger.mp3", legnth = 208 },
|
||||
[18] = { fname = "willardnetworks/musicradio/rebel/bluegrass/musicradio_willienelson_hellowalls.mp3", legnth = 240 }
|
||||
}
|
||||
|
||||
local metal = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_bathory_callfromthegrave.mp3", length = 260 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_blacksabbath_blacksabbath.mp3", length = 337 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_burzum_dunkelheitwav.mp3", length = 417 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_cannibalcorpse_hammersmashedface.mp3", length = 241 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_celticfrost_adyinggod.mp3", length = 336 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_celticfrost_cryptsofrays.mp3", length = 217 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_danzig_evilthing.mp3", length = 192 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_entombed_revelinflesh.mp3", length = 206 },
|
||||
[9] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_fearfactory_pisschrist.mp3", length = 319 },
|
||||
[10] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_mayhem_chainsawgutsfuck.mp3", length = 208 },
|
||||
[11] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_megadeth_hangar18.mp3", length = 310 },
|
||||
[12] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_megadeth_holywars.mp3", length = 390 },
|
||||
[13] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_megadeth_killingroad.mp3", length = 228 },
|
||||
[14] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_megadeth_symphonyofdestruction.mp3", length = 228 },
|
||||
[15] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_sepultura_arise.mp3", length = 197 },
|
||||
[16] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_sepultura_propaganda.mp3", length = 210 },
|
||||
[17] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_sepultura_rootsbloodyroots.mp3", length = 203 },
|
||||
[18] = { fname = "willardnetworks/musicradio/rebel/metal/musicradio_whitezombie_thunderkiss.mp3", length = 228 }
|
||||
}
|
||||
|
||||
local punk = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_agentorange_bloodstains.mp3", length = 108 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_aliceinchains_maninthebox.mp3", length = 265 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_aliceinchains_rooster.mp3", length = 367 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_aliceinchains_thembones.mp3", length = 147 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_aliceinchains_would.mp3", length = 204 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_badbrains_bigtakeover.mp3", length = 177 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_blackflag_mywar.mp3", length = 225 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_blackflag_riseabove.mp3", length = 142 },
|
||||
[9] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_clutch_bingeandpurge.mp3", length = 385 },
|
||||
[10] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_eyehategod_sisterfucker.mp3", length = 120 },
|
||||
[11] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_foofighters_everlong.mp3", length = 289 },
|
||||
[12] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_foofighters_monkeywrench.mp3", length = 230 },
|
||||
[13] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_helmet_unsung.mp3", length = 235 },
|
||||
[14] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_megadeth_takenoprisoners.mp3", length = 204 },
|
||||
[15] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_melvins_junebug.mp3", length = 112 },
|
||||
[16] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_ministry_thieves.mp3", length = 297 },
|
||||
[17] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_misfits_diediediemydarling.mp3", length = 184 },
|
||||
[18] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_misfits_whereeaglesdare.mp3", length = 122 },
|
||||
[19] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_ramones_blitzkriegbop.mp3", length = 130 },
|
||||
[20] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_ratm_bornofabrokenman.mp3", length = 277 },
|
||||
[21] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_ratm_killinginthenameof.mp3", length = 312 },
|
||||
[22] = { fname = "willardnetworks/musicradio/rebel/punk/musicradio_typeoneg_idontwannabeme.mp3", length = 223 }
|
||||
}
|
||||
|
||||
local alternative = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_aphextwin_1.mp3", length = 434 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_aphextwin_cockver10.mp3", length = 288 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_aphextwin_vordhosbn.mp3", length = 273 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_aphextwin_xtal.mp3", length = 273 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_bjork_armyofme.mp3", length = 229 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_massiveattack_dissolvedgirl.mp3", length = 358 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_massiveattack_teardrop.mp3", length = 318 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_nin_headlikeahole.mp3", length = 291 },
|
||||
[9] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_nin_hurt.mp3", length = 331 },
|
||||
[10] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_squarepusher_anirog09.mp3", length = 70 },
|
||||
[11] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_squarepusher_beepstreet.mp3", length = 389 },
|
||||
[12] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_squarepusher_craniumoxide.mp3", length = 29 },
|
||||
[13] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_squarepusher_decathlonoxide.mp3", length = 243 },
|
||||
[14] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_theprodigy_breathe.mp3", length = 235 },
|
||||
[15] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_theprodigy_firestarter.mp3", length = 278 },
|
||||
[16] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_whitezombie_morehumanthanhuman.mp3", length = 316 },
|
||||
[17] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_nin_bigcomedown.mp3", length = 246 },
|
||||
[18] = { fname = "willardnetworks/musicradio/rebel/alternative/musicradio_nin_thegreatbelow.mp3", length = 254 }
|
||||
}
|
||||
|
||||
local pirateAnnouncer = {
|
||||
[1] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_1.mp3", length = 49 },
|
||||
[2] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_2.mp3", length = 70 },
|
||||
[3] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_3.mp3", length = 87 },
|
||||
[4] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_4.mp3", length = 92 },
|
||||
[5] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_5.mp3", length = 111 },
|
||||
[6] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_6.mp3", length = 121 },
|
||||
[7] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_7.mp3", length = 110 },
|
||||
[8] = { fname = "willardnetworks/musicradio/rebel/announcer/musicradio_pirate_announce_8.mp3", length = 98 }
|
||||
}
|
||||
|
||||
-- Pirate (rebel) radio stations:
|
||||
ix.musicRadio:AddChannel("Blues & Rock", bluesRock, "pirate", {
|
||||
dispX = 195,
|
||||
freq = 550
|
||||
})
|
||||
ix.musicRadio:AddChannel("Hiphop", hipHop, "pirate", {
|
||||
dispX = 245,
|
||||
freq = 600
|
||||
})
|
||||
ix.musicRadio:AddChannel("Country", country, "pirate", {
|
||||
dispX = 295,
|
||||
freq = 650
|
||||
})
|
||||
ix.musicRadio:AddChannel("Metal", metal, "pirate", {
|
||||
dispX = 345,
|
||||
freq = 700
|
||||
})
|
||||
ix.musicRadio:AddChannel("Punk", punk, "pirate", {
|
||||
dispX = 395,
|
||||
freq = 750
|
||||
})
|
||||
ix.musicRadio:AddChannel("Alternative", alternative, "pirate", {
|
||||
dispX = 445,
|
||||
freq = 800
|
||||
})
|
||||
|
||||
if (CLIENT) then
|
||||
ix.musicRadio:PrecacheChannel("Blues & Rock")
|
||||
ix.musicRadio:PrecacheChannel("Hiphop")
|
||||
ix.musicRadio:PrecacheChannel("Country")
|
||||
ix.musicRadio:PrecacheChannel("Metal")
|
||||
ix.musicRadio:PrecacheChannel("Punk")
|
||||
ix.musicRadio:PrecacheChannel("Alternative")
|
||||
end
|
||||
|
||||
ix.musicRadio:AddAnnouncementsToClass("pirate", pirateAnnouncer)
|
||||
|
||||
if (CLIENT) then
|
||||
ix.musicRadio:PrecacheAnnouncementsForClass("pirate")
|
||||
end
|
||||
|
||||
--[[
|
||||
COMBINE RADIO STATIONS
|
||||
]]
|
||||
local classical = {
|
||||
[1] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_bach_cellosuite1gmaj.mp3", length = 140 },
|
||||
[2] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_chopin_balladeno1gminorop23.mp3", length = 537 },
|
||||
[3] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_chopin_nocturneop9no2.mp3", length = 256 },
|
||||
[4] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_chopin_nocturnno2.mp3", length = 244 },
|
||||
[5] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_kleine_serenadeno13gmaj.mp3", length = 383 },
|
||||
[6] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_kreisler_liebesleid.mp3", length = 201 },
|
||||
[7] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_mozart_concertono21cmaj.mp3", length = 444 },
|
||||
[8] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_satie_gymnopedie.mp3", length = 230 },
|
||||
[9] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_shostakovich_waltzno2.mp3", length = 242 },
|
||||
[10] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_tchaikovsky_waltzoftheflowers.mp3", length = 447 },
|
||||
[11] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_theswan.mp3", length = 172 },
|
||||
[11] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_rei_i.mp3", length = 176 },
|
||||
[12] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_vengerov_concerto.mp3", length = 205 },
|
||||
[13] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_tchaikovsky_lonelyhearts.mp3", length = 200 },
|
||||
[14] = { fname = "willardnetworks/musicradio/combine/classical/musicradio_tchaikovsky_adagio.mp3", length = 379 }
|
||||
}
|
||||
|
||||
local jazz = {
|
||||
[1] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_chetbaker_autmnleaves.mp3", length = 413 },
|
||||
[2] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_chetbaker_ifallinlovetooeasily.mp3", length = 195 },
|
||||
[3] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_davebrubeck_takefive.mp3", length = 316 },
|
||||
[4] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_ellafitzgerald_summertime.mp3", length = 293 },
|
||||
[5] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_johncoltrane_giantsteps.mp3", length = 282 },
|
||||
[6] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_johncoltrane_myfavoritethings.mp3", length = 817 },
|
||||
[7] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_johncoltrane_mylittlebrownbook.mp3", length = 310 },
|
||||
[8] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_milesdavis_sowhat.mp3", length = 540 },
|
||||
[9] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_sinatra_way_you_look_tonight.mp3", length = 199 },
|
||||
[10] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_pepper_four_brothers.mp3", length = 177 },
|
||||
[11] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_milesdavis_blue_in_green.mp3", length = 330 },
|
||||
[12] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_louis_armstrong_wonderful_world.mp3", length = 134 },
|
||||
[13] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_stangetz_corcovado.mp3", length = 251 },
|
||||
[14] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_chetbaker_almostblue.mp3", length = 293 },
|
||||
[15] = { fname = "willardnetworks/musicradio/combine/jazz/musicradio_cohen_raincoat.mp3", length = 306 }
|
||||
}
|
||||
|
||||
local funk = {
|
||||
[1] = { fname = "willardnetworks/musicradio/combine/funk/anri_shyness.mp3", length = 188 },
|
||||
[2] = { fname = "willardnetworks/musicradio/combine/funk/aran_im_in_love.mp3", length = 284 },
|
||||
[3] = { fname = "willardnetworks/musicradio/combine/funk/mamiya_japaneese.mp3", length = 234 },
|
||||
[4] = { fname = "willardnetworks/musicradio/combine/funk/matsubara_stay_with_me.mp3", length = 307 },
|
||||
[5] = { fname = "willardnetworks/musicradio/combine/funk/ohashi_i_love_you_so.mp3", length = 273 },
|
||||
[6] = { fname = "willardnetworks/musicradio/combine/funk/ohashi_sweet_love.mp3", length = 263 },
|
||||
[7] = { fname = "willardnetworks/musicradio/combine/funk/ohashi_telephone_number.mp3", length = 230 },
|
||||
[8] = { fname = "willardnetworks/musicradio/combine/funk/rick_james_give_it_to_me.mp3", length = 237 },
|
||||
[9] = { fname = "willardnetworks/musicradio/combine/funk/takeuichi_plastic_love.mp3", length = 269 },
|
||||
[10] = { fname = "willardnetworks/musicradio/combine/funk/yagami_bay_city.mp3", length = 237 },
|
||||
[11] = { fname = "willardnetworks/musicradio/combine/funk/yasuha_flyday_chinatown.mp3", length = 203 },
|
||||
[12] = { fname = "willardnetworks/musicradio/combine/funk/takahashi_sunset_road.mp3", length = 249 },
|
||||
}
|
||||
|
||||
local speech = {
|
||||
[1] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_instinct.mp3", length = 191 },
|
||||
[2] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_civilprotection.mp3", length = 158 },
|
||||
[3] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_accomplishment.mp3", length = 156 },
|
||||
[4] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_civilprotection2.mp3", length = 173 },
|
||||
[5] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_progress.mp3", length = 208 },
|
||||
[6] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_support.mp3", length = 186 },
|
||||
[7] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_workshifts.mp3", length = 162 },
|
||||
[8] = { fname = "willardnetworks/musicradio/combine/speech/musicradio_breen_workshifts2.mp3", length = 167 }
|
||||
}
|
||||
|
||||
local benefactorAnnouncer = {
|
||||
[1] = { fname = "willardnetworks/musicradio/combine/announcer/musicradio_benefactor_announce_1.mp3", length = 65 },
|
||||
[2] = { fname = "willardnetworks/musicradio/combine/announcer/musicradio_benefactor_announce_2.mp3", length = 65 }
|
||||
}
|
||||
|
||||
-- Benefactor (cmb) radio stations:
|
||||
ix.musicRadio:AddChannel("Classical", classical, "benefactor", {
|
||||
dispX = 195,
|
||||
freq = 550
|
||||
})
|
||||
|
||||
ix.musicRadio:AddChannel("Jazz", jazz, "benefactor", {
|
||||
dispX = 245,
|
||||
freq = 600
|
||||
})
|
||||
|
||||
ix.musicRadio:AddChannel("Funk", funk, "benefactor", {
|
||||
dispX = 295,
|
||||
freq = 650
|
||||
})
|
||||
|
||||
ix.musicRadio:AddChannel("Speech", speech, "benefactor", {
|
||||
dispX = 345,
|
||||
freq = 700
|
||||
})
|
||||
|
||||
if (CLIENT) then
|
||||
ix.musicRadio:PrecacheChannel("Classical")
|
||||
ix.musicRadio:PrecacheChannel("Jazz")
|
||||
ix.musicRadio:PrecacheChannel("Funk")
|
||||
ix.musicRadio:PrecacheChannel("Speech")
|
||||
end
|
||||
|
||||
--[[ Not adding right now because the MoE is dragging me through the mud on this
|
||||
ix.musicRadio:AddAnnouncementsToClass("benefactor", benefactorAnnouncer)
|
||||
|
||||
if (CLIENT) then
|
||||
ix.musicRadio:PrecacheAnnouncementsForClass("benefactor")
|
||||
end
|
||||
]]
|
||||
|
||||
--[[
|
||||
misc precaching
|
||||
]]
|
||||
for i=1, 6 do
|
||||
util.PrecacheSound("willardnetworks/musicradio/musicradio_static_"..tostring(i)..".mp3")
|
||||
end
|
||||
|
||||
for i=1, 4 do
|
||||
util.PrecacheSound("willardnetworks/musicradio/musicradio_click_"..tostring(i)..".mp3")
|
||||
end
|
||||
end
|
||||
|
||||
if (CLIENT) then
|
||||
local color = Color(75, 196, 0)
|
||||
function PLUGIN:InitializedPlugins()
|
||||
local function drawRadioESP(client, entity, x, y, factor)
|
||||
local txt = "Music Radio - ".."Chan: "..tostring(entity:GetChan()).." Vol ("..tostring(entity:GetVolume())..")".." On: "..tostring(entity:IsPlayingMusic())
|
||||
ix.util.DrawText(txt, x, y - math.max(10, 32 * factor), color,
|
||||
TEXT_ALIGN_CENTER, TEXT_ALIGN_CENTER, nil, math.max(255 * factor, 80))
|
||||
end
|
||||
|
||||
ix.observer:RegisterESPType("wn_musicradio", drawRadioESP, "Music Radio")
|
||||
end
|
||||
end
|
||||
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