Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
a9f6d10
Initial comment stuff, preparing for changes
TeeMeeFe Jan 30, 2026
7d3f279
Bump this number for the love of god. Does nothing but annoy anyone c…
TeeMeeFe Jan 30, 2026
cd0938e
Guess what, i see no harm in doing this anyway
TeeMeeFe Jan 30, 2026
f4e7cd2
Initial work for networking interpolated sounds, WIP
TeeMeeFe Feb 3, 2026
145f801
Add initial implementation of interpolated sounds
TeeMeeFe Feb 6, 2026
f98d236
Begin offloading pitch/volume calculations to clientside, also try to…
TeeMeeFe Feb 7, 2026
fe4d531
Drastically improve how sounds are networked and handled
TeeMeeFe Feb 7, 2026
2d84e16
Fix single sounds not playing, move SuperDuperHandySoundBank back to …
TeeMeeFe Feb 7, 2026
d92e5f6
Whoopsy daisy
TeeMeeFe Feb 7, 2026
3271767
Shuffling things around, cleanup
TeeMeeFe Feb 7, 2026
1a57068
Actually no, nevermind this goes back to how it was for now
TeeMeeFe Feb 8, 2026
6a77f4a
Fix not getting any SoundBanks, improve clientside sound object stori…
TeeMeeFe Feb 8, 2026
7da3fb6
Several more tweaks done
TeeMeeFe Feb 9, 2026
9abb59b
Update to latest dev
TeeMeeFe Feb 9, 2026
db368aa
Forgot to rate limit our SendMultipleAdjustableSounds function too, a…
TeeMeeFe Feb 9, 2026
571a136
Add some new lines
TeeMeeFe Feb 9, 2026
f95dd1a
Correct these two, they're not optional
TeeMeeFe Feb 9, 2026
89fe1f9
Initial menu overhaul, commented out the old code for now, many thing…
TeeMeeFe Feb 12, 2026
d4c80fb
Prevent the last item from removing itself, some work towards relabel…
TeeMeeFe Feb 13, 2026
1d37eea
Merge latest dev changes
TeeMeeFe Feb 13, 2026
10c5416
Redo panels to use ACF panels instead, tweak panel margins, fix error…
TeeMeeFe Feb 13, 2026
26ea37a
Remove notice
TeeMeeFe Feb 13, 2026
15dad9e
Fix sound indices not updating upon removing any sound
TeeMeeFe Feb 14, 2026
41724ce
Allow panels to refresh by making them temporary, that was easy...
TeeMeeFe Feb 14, 2026
30114bc
Refactor the menu a bit, add a wip text to describe what the panel does
TeeMeeFe Feb 14, 2026
97e4ab2
Add a graph, some misc changes
TeeMeeFe Feb 14, 2026
e18f587
Removed redundant function from sounds_cl; Fixed graph panel not show…
TeeMeeFe Feb 14, 2026
bba5d91
Alright i think im done with stylizing these panels, clean up sounds_…
TeeMeeFe Feb 14, 2026
531fcca
Optimize the amount of data being sent to the client upon multiple so…
TeeMeeFe Feb 15, 2026
bd3963a
Refactor variable naming, switch from camelCase to PascalCase on most…
TeeMeeFe Feb 16, 2026
ea29260
it*-less
TeeMeeFe Feb 16, 2026
64f2f1a
Replace OptionSelectionBox be an ACF combobox instead of a panel, uni…
TeeMeeFe Feb 16, 2026
bedf718
Refactor 4th menu again; Idle, Redline and RPM can communicate with e…
TeeMeeFe Feb 17, 2026
08ad85a
Refactor value panel creation, renamed method, improve style
TeeMeeFe Feb 17, 2026
e17d69d
Several changes and improvements to the menu, graph can plot lines no…
TeeMeeFe Feb 19, 2026
cc6c70c
Update to latest dev
TeeMeeFe Feb 19, 2026
77155f1
Improve the menu some more
TeeMeeFe Feb 21, 2026
cae82a1
Refactor how sound tables are networked
TeeMeeFe Feb 23, 2026
272e73b
Here i reach a dead-end
TeeMeeFe Feb 23, 2026
3a73385
Right clicking on an engine with a soundbank can now populate the sou…
TeeMeeFe Feb 25, 2026
2e53577
More work towards setting a soundbank to any engine, the final functi…
TeeMeeFe Feb 26, 2026
9a25fdb
Add the missing function implementation, it worksgit add .!
TeeMeeFe Feb 26, 2026
529efe8
Update to latest dev
TeeMeeFe Feb 27, 2026
1d3471e
Remove goober
TeeMeeFe Feb 27, 2026
c443bf8
A round of documentation
TeeMeeFe Feb 27, 2026
676dee7
Shuffle things around
TeeMeeFe Feb 27, 2026
c01ae97
Rework how values are added and removed
TeeMeeFe Feb 28, 2026
74efabd
Fix function call not getting passed the correct table to replace the…
TeeMeeFe Mar 3, 2026
f6857e2
Fix function call not getting passed the correct table to replace the…
TeeMeeFe Mar 3, 2026
063a999
a
TeeMeeFe Mar 3, 2026
083ca03
Add dupe saving to soundtables, Add mechanism to reset the sounds as …
TeeMeeFe Mar 5, 2026
38a9323
A round of documentation and some minor cleanup
TeeMeeFe Mar 5, 2026
08b3209
Several improvements to the sound menu
TeeMeeFe Mar 6, 2026
2b57e50
Add XY labels to the graph
TeeMeeFe Mar 6, 2026
817bcaa
Add custom background color to list panels
TeeMeeFe Mar 7, 2026
1498589
Fix error on engines without a soundbank table
TeeMeeFe Mar 7, 2026
52f8392
Improve value removal logic
TeeMeeFe Mar 7, 2026
bd25883
Comment debug prints
TeeMeeFe Mar 7, 2026
dfef5d0
Merge branch 'dev' into TMF/Improved_sounds
TeeMeeFe Mar 7, 2026
646bee6
Tweak panel colors to be a predictable rainbow, grey out play/stop bu…
TeeMeeFe Mar 7, 2026
829f72a
Potential fix for client error on missing sounds and accessing empty …
TeeMeeFe Mar 9, 2026
9a2623c
Fix error for call to delete sounds from a non existant table, other …
TeeMeeFe Mar 9, 2026
b905743
Validate entities on client when receiving them, move soundtable afte…
TeeMeeFe Mar 12, 2026
eccfe52
Fix a goober of mine
TeeMeeFe Mar 12, 2026
e4cc28e
Move this here
TeeMeeFe Mar 12, 2026
c159e6d
Update to latest dev
TeeMeeFe Mar 14, 2026
22756bf
Update to latest dev
TeeMeeFe Mar 19, 2026
d3e82fc
Update to latest dev
TeeMeeFe Mar 19, 2026
1109aea
Update to latest dev
TeeMeeFe Mar 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion lua/acf/core/globals.lua
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ do -- ACF global vars

-- The deviation of the input direction from the shaft + the output direction from the shaft cannot exceed this
ACF.DefineSetting("MaxDriveshaftAngle", 85, nil, ACF.FloatDataCallback(85, 180, 0))
ACF.Year = 1945
ACF.Year = 2026 -- Was 1945. Define this hardcoded for now, always the year in course unless more work on this gets done and does actually get used properly
ACF.IllegalDisableTime = 30 -- Time in seconds for an entity to be disabled when it fails ACF.IsLegal
ACF.Volume = 1 -- Global volume for ACF sounds
ACF.MobilityLinkDistance = 650 -- Maximum distance, in inches, at which mobility-related components will remain linked with each other
Expand All @@ -195,6 +195,9 @@ do -- ACF global vars
ACF.KwToHp = 1.341 -- Kilowatts to horsepower
ACF.LToGal = 0.264172 -- Liters to gallons

-- Miscellaneous Sound Stuff
ACF.SpeedOfSound = 343 * ACF.MeterToInch -- in Meters per Second. Source internally uses inches(or units) so we have to convert
-- Actually this would vary as a function of temperature and air pressure, but this should suffice for now
-- Fuzes
ACF.MinFuzeCaliber = 20 -- Minimum caliber in millimeters that can be fuzed

Expand Down
167 changes: 161 additions & 6 deletions lua/acf/core/utilities/sounds/sounds_cl.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
local Sounds = ACF.Utilities.Sounds
local _MAXSOUNDS = 16 -- Maximum amount of sounds we're willing to send and have. TODO(TMF): Make this a global!
local map = math.Remap
local clamp = math.Clamp

do -- Valid sound check
local file = file
Expand Down Expand Up @@ -27,9 +30,6 @@ do -- Valid sound check
end
end

-- MARCH/TODO: universal ACF constant for speed of sound (maybe it already exists and I don't know :P)
local SpeedOfSound = 343 * 39.37

local function DistanceToOrigin(Origin)
if isentity(Origin) and IsValid(Origin) then
return LocalPlayer():EyePos():Distance(Origin:GetPos())
Expand All @@ -46,7 +46,7 @@ end
local function DoDelayed(Origin, Call, Instant)
if Instant then return Call() end

local Delay = DistanceToOrigin(Origin) / SpeedOfSound
local Delay = DistanceToOrigin(Origin) / ACF.SpeedOfSound
if Delay > 0.1 then
timer.Simple(Delay, function() Call() end)
else
Expand Down Expand Up @@ -130,9 +130,10 @@ do -- Processing adjustable sounds (for example, engine noises)
--- @param Path string The path to the sound to be played local to the game's sound folder
--- @param Pitch integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume
--- @return Sound CSoundPatch The sound object
function Sounds.CreateAdjustableSound(Origin, Path, Pitch, Volume)
if not IsValid(Origin) then return end
if Origin.Sound then return end
if not Sounds.IsValidSound(Path) then return end

local Sound = CreateSound(Origin, Path)
Origin.Sound = Sound
Expand All @@ -142,7 +143,8 @@ do -- Processing adjustable sounds (for example, engine noises)
Sounds.DestroyAdjustableSound(Entity, true)
end)

Sounds.UpdateAdjustableSound(Origin, Pitch, Volume)
Sounds.UpdateAdjustableSound(Sound, Pitch, Volume)
return Sound
end

--- Stops an existing adjustable sound on the origin.
Expand Down Expand Up @@ -181,6 +183,159 @@ do -- Processing adjustable sounds (for example, engine noises)
end)
end

-- Fade function taken from:
-- https://dsp.stackexchange.com/questions/37477/understanding-equal-power-crossfades
-- https://dsp.stackexchange.com/questions/14754/equal-power-crossfade
function Sounds.Fade(n, min, mid, max)
local _PI = math.pi

if n < min or n > max then return 0 end

if n > mid then
min = mid - (max - mid)
end

return math.cos((1 - ((n - min) / (mid - min))) * (_PI / 2))
end

-- Consider if we actually want to do this too! (commented out for now)
--local SmoothRPM = 0
--local SmoothThrottle = 0

-- This is where the magic to interpolate sounds happen.
-- In order to make yourself a better idea of what this does you can consult the image below:
-- https://i.imgur.com/KaFmaMf.png
local function DoPitchVolumeAtRPM(Origin, Throttle, RPM)
local SoundObjects = Origin.SoundObjects
if not SoundObjects or table.IsEmpty(SoundObjects) then return end

local fade = Sounds.Fade -- idk if this is faster to do, but given this is a hot path, might as well be...
--SmoothRPM = SmoothRPM * (1 - 0.1) + RPM * 0.1
--SmoothThrottle = SmoothThrottle * (1 - 0.1) + Throttle * 10

-- Sound volumes when throttle is 0 and 100 respectively
-- TODO(TMF): This should be able to be configured from the sound menu or to be a function of the engine's load
local _OFFVOLUME = 0.25
local _ONVOLUME = 1

-- TODO(TMF): Potentially add some mechanism here to check for any differences and only update those
for idx, soundTable in ipairs(SoundObjects) do
if not soundTable.RPM then continue end
Origin.Sound = soundTable.Sound

local addCurveWidth = soundTable.Width or 0
local enginePitch = soundTable.Pitch or 1
local min = idx == 1 and 0 or SoundObjects[clamp(idx - 1 - addCurveWidth, 1, _MAXSOUNDS)].RPM
local mid = soundTable.RPM
local max = idx == #SoundObjects and 1000000 or SoundObjects[clamp(idx + 1 + addCurveWidth, 1, _MAXSOUNDS)].RPM
local curve = fade(RPM, min, mid, max)
local volume = curve * map(Throttle, 0, 100, _OFFVOLUME, _ONVOLUME) * (soundTable.Volume or 1)
local pitch = (RPM / soundTable.RPM) * enginePitch

Sounds.UpdateAdjustableSound(Origin, pitch, volume)
end
end

do -- Multiple Engine Sounds(ex. Interpolated sounds)
--- Creates many sounds from a table, and stores their entries in the Origin's entity.
--- Reuses existing methods to create and update sounds.
--- @param Origin table The entity to play the sounds from
--- @param SoundTable table The networked table with nested table containing rpm, sound path, pitch, volume, width and empty sound
function Sounds.CreateMultipleAdjustableSounds(Origin, SoundTable)
local SoundCount = 0

for _, sndTable in ipairs(SoundTable) do
if not Sounds.IsValidSound(sndTable.Path) then return end
local Sound = Sounds.CreateAdjustableSound(Origin,
sndTable.Path,
sndTable.Pitch or 100, 0 -- Create the sound deafened
)
if not Sound then
print("Failed to create sound for entity " .. tostring(Origin) .. ". Sound path does not exist!")
continue
end
sndTable.Sound = Sound
SoundCount = SoundCount + 1

Sounds.UpdateAdjustableSound(Origin, sndTable.Pitch or 100, 0)
end

-- Sort the table by the rpm before moving on, so it can be iterated in sequential order
table.sort(SoundTable, function(a, b) return a.RPM < b.RPM end)

Origin.SoundObjects = SoundTable
Origin.SoundCount = SoundCount
-- Ensuring that the sounds can't stick around if the server doesn't properly ask for them to be destroyed
Origin:CallOnRemove("ACF_ForceStopMultipleAdjustableSounds", function(Entity)
Sounds.DeleteMultipleAdjustableSounds(Entity, true)
end)
end

local IsValid = IsValid
--- Stops all the existing sounds from the entity
--- @param Origin table The entity to stop all the sounds from
function Sounds.DeleteMultipleAdjustableSounds(Origin, _)
if not IsValid(Origin) then return end
if not Origin.SoundObjects then return end

for idx, snd in ipairs(Origin.SoundObjects) do
snd.Sound:Stop()
Origin.SoundObjects[idx] = nil
end
Origin.Sound = nil -- Just in case
Origin.SoundCount = 0
end

-- For multiple sounds creation
net.Receive("ACF_Sounds_AdjustableCreate_Multi", function()
--print("Received " .. len .. " bits from \"ACF_Sounds_AdjustableCreate_Multi\" for sound creation!") -- Debug print
local Origin = net.ReadEntity()

local SoundTable = {}
local Count = net.ReadUInt(4)

local I = 0

while (I < Count) do
local RPM = net.ReadUInt(14)
local StringPath = net.ReadString()
local Pitch = net.ReadUInt(8)
local Volume = net.ReadUInt(8)
local Width = net.ReadUInt(4)

Volume = Volume * 0.01 -- Reduce the received value down to a float
table.insert(SoundTable, { RPM = RPM,
Path = StringPath,
Pitch = Pitch or 100,
Volume = Volume or 1,
Width = Width or 0,
Sound = nil }) -- Fuck it we ball
I = I + 1
end

if not IsValid(Origin) then return end
Sounds.CreateMultipleAdjustableSounds(Origin, SoundTable)
end)

-- For updates on multiple sounds
net.Receive("ACF_Sounds_Adjustable_Multi", function()
--print("Received " .. len .. " bits from \"ACF_Sounds_Adjustable_Multi\" for sound updates!") -- Debug print
local Origin = net.ReadEntity()
local ShouldStop = net.ReadBool()

if not IsValid(Origin) then return end

-- Do we really need to remove every existing sound when the engine just turns off?
if ShouldStop then
Sounds.DeleteMultipleAdjustableSounds(Origin)
else
local Throttle = net.ReadUInt(7)
local RPM = net.ReadUInt(14)

DoPitchVolumeAtRPM(Origin, Throttle, RPM)
end
end)
end
--- Returns a table of sound infomation depending on what the trace hit.
--- @param Data table The effect data relating to the projectile
--- @param Trace table The trace data relating to the projectile
Expand Down
114 changes: 93 additions & 21 deletions lua/acf/core/utilities/sounds/sounds_sv.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ local Sounds = ACF.Utilities.Sounds
util.AddNetworkString("ACF_Sounds")
util.AddNetworkString("ACF_Sounds_Adjustable")
util.AddNetworkString("ACF_Sounds_AdjustableCreate")
util.AddNetworkString("ACF_Sounds_Adjustable_Multi")
util.AddNetworkString("ACF_Sounds_AdjustableCreate_Multi")

--- Sends a single, non-looping sound to all clients in the PAS.
--- @param Origin table | vector The source to play the sound from
--- @param Path string The path to the sound to be played local to the game's sound folder
--- @param Level? integer The sound's level/attenuation from 0-127
--- @param Pitch? integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume. This is internally converted into an integer from 0-255 for network optimization
--- Sends a single, non-looping sound to all clients in the PAS.
--- @param Origin table | vector The source to play the sound from
--- @param Path string The path to the sound to be played local to the game's sound folder
--- @param Level? integer The sound's level/attenuation from 0-127
--- @param Pitch? integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume. This is internally converted into an integer from 0-255 for network optimization
function Sounds.SendSound(Origin, Path, Level, Pitch, Volume)
if not IsValid(Origin) then return end

Expand All @@ -36,13 +38,13 @@ function Sounds.SendSound(Origin, Path, Level, Pitch, Volume)
net.SendPAS(Pos)
end

--- Creates a sound patch on all clients in the PAS.
--- This is intended to be used for self-looping sounds played on an entity that can be adjusted easily later.
--- This allows us to modify the pitch/volume of a looping sound (ex. engines) with minimal network usage.
--- @param Origin table The entity to play the sound from
--- @param Path string The path to the sound to be played local to the game's sound folder
--- @param Pitch integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume
--- Creates a sound patch on all clients in the PAS.
--- This is intended to be used for self-looping sounds played on an entity that can be adjusted easily later.
--- This allows us to modify the pitch/volume of a looping sound (ex. engines) with minimal network usage.
--- @param Origin table The entity to play the sound from
--- @param Path string The path to the sound to be played local to the game's sound folder
--- @param Pitch integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume
function Sounds.CreateAdjustableSound(Origin, Path, Pitch, Volume)
if not IsValid(Origin) then return end

Expand All @@ -54,17 +56,19 @@ function Sounds.CreateAdjustableSound(Origin, Path, Pitch, Volume)
net.SendPAS(Origin:GetPos())
end

--- Sends an update to an adjustable sound to all clients in the PAS.
--- If the adjustable sound was stopped on the client, it will begin playing again on the origin with the given parameters.
--- This function is ratelimited to reduce network consumption, and subsequent updates will be smoothed on the client with an equivalent delta time.
--- @param Origin table The entity to update the sound on
--- @param ShouldStop? boolean Whether the sound should be destroyed; defaults to false
--- @param Pitch integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume. This is internally converted into an integer from 0-255 for network optimization
--- Sends an update to an adjustable sound to all clients in the PAS.
--- If the adjustable sound was stopped on the client, it will begin playing again on the origin with the given parameters.
--- This function is ratelimited to reduce network consumption, and subsequent updates will be smoothed on the client with an equivalent delta time.
--- @param Origin table The entity to update the sound on
--- @param ShouldStop? boolean Whether the sound should be destroyed; defaults to false
--- @param Pitch integer The sound's pitch from 0-255
--- @param Volume number A float representing the sound's volume. This is internally converted into an integer from 0-255 for network optimization
function Sounds.SendAdjustableSound(Origin, ShouldStop, Pitch, Volume)
ShouldStop = ShouldStop or false

local Time = CurTime()
local OriginTbl = Origin.ACF

if not OriginTbl then
OriginTbl = {}
Origin.ACF = OriginTbl
Expand All @@ -85,4 +89,72 @@ function Sounds.SendAdjustableSound(Origin, ShouldStop, Pitch, Volume)
net.SendPAS(Origin:GetPos())
OriginTbl.SoundTimer = Time + 0.05
end
end
end

--- Creates a sound table to be broadcasted to all players within PAS.
--- This allows us to then create multiple sounds attached to a single entity, and be played fully clientside.
--- For creating 13 sounds, the data being sent can ballon up to 1.537kb's of data at once.
--- @param Origin table The entity to play the sound from
--- @param SoundTable table The table whose keys are arbitrary RPM's and values containing a table with a sound path, pitch and volume, to be played at a defined RPM(Its keys).
function Sounds.CreateMultipleAdjustableSounds(Origin, SoundTable, SoundCount)
if not IsValid(Origin) then return end
if not istable(SoundTable) then return end

-- Separate our table in chunks to be sent instead of all at once
-- This saves about 40% in data size vs. sending the whole table
net.Start("ACF_Sounds_AdjustableCreate_Multi")
net.WriteEntity(Origin)
net.WriteUInt(SoundCount, 4)

for _, v in ipairs(SoundTable) do
local rpm = v.RPM
local stringPath = v.Path
local pitch = v.Pitch
local volume = v.Volume
local width = v.Width

net.WriteUInt(rpm, 14)
net.WriteString(stringPath)
net.WriteUInt(pitch, 8)

volume = volume * 100 -- Sending the approximate volume as an int to reduce message size
net.WriteUInt(volume, 8)
net.WriteUInt(width, 4)
end
net.SendPAS(Origin:GetPos())
end

--- Sends an update to the client regarding Throttle, RPM and if it should stop the sound, from an engine.
--- This also allows us to modify the pitch/volume of multiple looping sounds (for an engine) with minimal network usage.
--- The sound calculations are performed entirely clientside and require net unreliable for better sound composition.
--- This function is also rate limited to reduce network consumption, and subsequent updates will be smoothed on the client with an equivalent delta time.
--- @param Origin table The entity to update the sound from
--- @param ShouldStop? boolean Whether the sound should be destroyed; defaults to false
--- @param Throttle int The entity's throttle
--- @param RPM int The entity's RPM
function Sounds.SendMultipleAdjustableSounds(Origin, ShouldStop, Throttle, RPM)
if not IsValid(Origin) then return end
ShouldStop = ShouldStop or false

local Time = CurTime()
local OriginTbl = Origin.ACF

if not OriginTbl then
OriginTbl = {}
Origin.ACF = OriginTbl
end
OriginTbl.SoundTimer = OriginTbl.SoundTimer or Time

-- Slowing down the rate of sending a bit
if OriginTbl.SoundTimer <= Time or ShouldStop then
net.Start("ACF_Sounds_Adjustable_Multi", true)
net.WriteEntity(Origin)
net.WriteBool(ShouldStop)
if not ShouldStop then
net.WriteUInt(Throttle or 0, 7)
net.WriteUInt(RPM or 0, 14) -- Theorically there are engines capable of reaching more than 16K RPM. If you do so, you can go off yourself...
end
net.SendPAS(Origin:GetPos())
OriginTbl.SoundTimer = Time + 0.05
end
end
15 changes: 15 additions & 0 deletions lua/acf/core/utilities/sounds/tool_support_sh.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ Sounds.acf_engine = {
Ent.SoundVolume = 1

Ent:UpdateSound()
end,
GetSoundBank = function(Ent)
return {
SoundBank = Ent.SoundBank
}
end,
SetSoundBank = function(Ent, SoundBankData)
Ent.SoundBank = SoundBankData

Ent:UpdateSoundBank()
end,
ResetSoundBank = function(Ent)
Ent.SoundBank = {}

Ent:UpdateSoundBank()
end
}

Expand Down
Loading