mirror of
https://github.com/dcs-retribution/dcs-retribution.git
synced 2025-11-10 15:41:24 +00:00
792 lines
34 KiB
Lua
792 lines
34 KiB
Lua
env.info("-----DCSRetribution|MOOSE Soundhandler plugin - configuration start -----")
|
||
-- assert(loadfile("C:\\Users\\Taco\\Documents\\Github\\DCS\\Custom_Retribution\\Plugins\\MooseSoundhandler\\Plugin_Soundhandler.lua"))()
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- CONFIG
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- Defaults (overridden by dcsRetribution.plugins.MooseSoundhandler.* if present)
|
||
SoundToGroupOnly = true
|
||
ShipSamSounds = true
|
||
PlayOwnShootingGuns = true
|
||
PlayOpForShootingGuns = true
|
||
SoundDebug = true
|
||
SoundHandler = true -- Not a UI setting, but an On/Off switch mid-mission.
|
||
|
||
if dcsRetribution and dcsRetribution.plugins and dcsRetribution.plugins.MooseSoundhandler then
|
||
-- Use externally provided settings, preserving original field names
|
||
if dcsRetribution.plugins.MooseSoundhandler.SoundToGroupOnly ~= nil then
|
||
SoundToGroupOnly = dcsRetribution.plugins.MooseSoundhandler.SoundToGroupOnly
|
||
end
|
||
if dcsRetribution.plugins.MooseSoundhandler.ShipSamSounds ~= nil then
|
||
ShipSamSounds = dcsRetribution.plugins.MooseSoundhandler.ShipSamSounds
|
||
end
|
||
if dcsRetribution.plugins.MooseSoundhandler.PlayOwnShootingGuns ~= nil then
|
||
PlayOwnShootingGuns = dcsRetribution.plugins.MooseSoundhandler.PlayOwnShootingGuns
|
||
end
|
||
if dcsRetribution.plugins.MooseSoundhandler.PlayOpForShootingGuns ~= nil then
|
||
PlayOpForShootingGuns = dcsRetribution.plugins.MooseSoundhandler.PlayOpForShootingGuns
|
||
end
|
||
if dcsRetribution.plugins.MooseSoundhandler.SoundDebug ~= nil then
|
||
SoundDebug = dcsRetribution.plugins.MooseSoundhandler.SoundDebug
|
||
end
|
||
else
|
||
env.info("-----dcsRetribution.plugins.MooseSoundhandler NOT FOUND")
|
||
end
|
||
|
||
env.info("--------- SoundToGroupOnly=" .. tostring(SoundToGroupOnly) ..
|
||
" | ShipSamSounds=" .. tostring(ShipSamSounds) ..
|
||
" | PlayOwnShootingGuns=" .. tostring(PlayOwnShootingGuns) ..
|
||
" | PlayOpForShootingGuns=" .. tostring(PlayOpForShootingGuns) ..
|
||
" | Debug=" .. tostring(SoundDebug))
|
||
|
||
if SoundDebug then
|
||
trigger.action.outText("---SOUND DEBUG IS ON!---", 10)
|
||
trigger.action.outText("Soundhandler " .. (SoundHandler and "ON" or "OFF"), 10)
|
||
trigger.action.outText("Sounds will play to " .. (SoundToGroupOnly and "GROUP only" or "ALL clients in coalition"),
|
||
10)
|
||
trigger.action.outText("Ships firing SAMs will " .. (ShipSamSounds and "" or "NOT ") .. "play sounds", 10)
|
||
trigger.action.outText("Own Airplane Gun Bursts will " .. (PlayOwnShootingGuns and "" or "NOT ") .. "play sounds", 10)
|
||
trigger.action.outText(
|
||
"OpFor Airplane Gun Bursts will " .. (PlayOpForShootingGuns and "" or "NOT ") .. "play sounds", 10)
|
||
end
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- SOUND DATA
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
SoundFilePath = SoundFilePath or ""
|
||
if SoundFilePath ~= "" then
|
||
local last = SoundFilePath:sub(-1)
|
||
if last ~= "/" and last ~= "\\" then
|
||
SoundFilePath = SoundFilePath .. "/"
|
||
end
|
||
end
|
||
|
||
Sounds = {}
|
||
Sounds.Air_Unit_Sound_Table = { "AAGoodKill", "AAKill4", "AAKillGoodhiton1", "AAKillSplash", "AAKillSplashone",
|
||
"AASplashOne", "AASplashOne_2" }
|
||
Sounds.Incoming_Missile_Table = { "Misil1", "Misil2", "Misil3", "Misil4", "Misil5", "Misil6" }
|
||
Sounds.Ground_Unit_Sound_Table = { "AGKillBOOM1", "AGKillCOMEONBABY", "AGKillGoodBOOM", "AGKillSeeTheSmoke",
|
||
"AGKill_TARGET_DESTROYED", "AGKillBeautiful_beautiful", "AGKillMotherFucker" }
|
||
Sounds.SamSoundTable = { "SAM1", "SAM2", "SAM3", "SAM4", "SAM5", "SAM6", "SAM7", "Defending" }
|
||
Sounds.Ballistic = { "SCUD_Long", "Fireball" }
|
||
Sounds.PigsAway_Sound_Table = { "PigsAway", "PigsAway2" }
|
||
Sounds.Paveway_Sound_Table = { "Paveway" }
|
||
Sounds.Bruiser_Sound_Table = { "Bruiser", "Bruiser2", "Bruiser3" }
|
||
Sounds.Fox2_Sound_Table = { "Fox2A", "Fox2B", "Fox2C", "Fox2D", "Fox2E" }
|
||
Sounds.Fox3_Sound_Table = { "Fox3A", "Fox3B", "Fox3C", "Fox3D", "Fox3E", "Fox3F" }
|
||
Sounds.Fox1_Sound_Table = { "Fox1A", "Fox1B" }
|
||
Sounds.Magnum_Sound_Table = { "Magnum", "Magnum2" }
|
||
Sounds.Rifle_Sound_Table = { "RifleA", "RifleB", "RifleC", "RifleD", "RifleE" }
|
||
Sounds.Pickle_Sound_Table = { "Pickle1", "Pickle2", "Pickle3", "Pickle4", "Pickle5", "Pickle6", "Pickle7" }
|
||
Sounds.FriendlyLosses = { "OhJesus", "HitEjecting1", "HitEjecting2", "HitEjecting3", "StartFindingMeBoys" }
|
||
Sounds.Guns_OwnFire = { "BlueGuns1", "BlueGuns2", "BlueGuns3" }
|
||
Sounds.Guns_Incoming = { "Guns_Break_Right", "Guns_Break_Left" }
|
||
Sounds.Decoy_Table = { "A2G_Duck1" }
|
||
Sounds.CruiseMissile_Table = { "A2G_Greyhound1" }
|
||
Sounds.Vampires_Table = { "A2G_Vampires1" } -- anti-ship call
|
||
Sounds.Tomahawk_Table = { "S2G_Tomahawk1" }
|
||
Sounds.Friendly_Fire_Table = { "FriendlyFire1", "FriendlyFire2" }
|
||
Sounds.Friendly_SAM_Table = { "BirdsAway" }
|
||
|
||
if SoundDebug then
|
||
for _, tbl in pairs(Sounds) do
|
||
local txt = UTILS.OneLineSerialize(tbl)
|
||
env.info("Sounds Loaded: " .. txt)
|
||
trigger.action.outText("Sounds Loaded: " .. txt, 10)
|
||
end
|
||
end
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- HELPERS
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
local BLUE, RED = coalition.side.BLUE, coalition.side.RED
|
||
|
||
local function Opposing(side)
|
||
if side == BLUE then return RED end
|
||
if side == RED then return BLUE end
|
||
return side
|
||
end
|
||
|
||
local function ChooseRandom(t) return t[math.random(1, #t)] end
|
||
|
||
-- === DROP-IN: NewSound (no filesystem check; works with mission-embedded sounds) ===
|
||
local function NewSound(nameNoExt)
|
||
local full = SoundFilePath .. nameNoExt .. ".ogg"
|
||
if SoundDebug then env.info("[SND] NewSound: " .. tostring(full)) end
|
||
return USERSOUND:New(full)
|
||
end
|
||
|
||
-- === DROP-IN: PlayToGroupOrCoalition (robust group canonization; no mirror re-load) ===
|
||
-- Signature: PlayToGroupOrCoalition(soundObj, groupObj, coalitionSide)
|
||
local function PlayToGroupOrCoalition(soundObj, groupObj, coalitionSide)
|
||
if not soundObj then
|
||
if SoundDebug then env.info("[SND] Play aborted: soundObj is nil") end
|
||
return
|
||
end
|
||
|
||
-- Re-resolve the group by name to avoid stale wrapper instances.
|
||
local canonGroup = nil
|
||
if groupObj and groupObj.GetName then
|
||
local gname = groupObj:GetName()
|
||
if gname then canonGroup = GROUP:FindByName(gname) end
|
||
end
|
||
|
||
-- Preferred path: play to group when toggled and group exists
|
||
if SoundToGroupOnly and canonGroup then
|
||
if SoundDebug then
|
||
local dcs = canonGroup.GetDCSObject and canonGroup:GetDCSObject() or nil
|
||
local gid = dcs and dcs:getID() or "?"
|
||
env.info(string.format("[SND] ToGroup '%s' (id=%s)", tostring(canonGroup:GetName()), tostring(gid)))
|
||
end
|
||
soundObj:ToGroup(canonGroup)
|
||
return
|
||
end
|
||
|
||
-- Coalition fallback
|
||
if coalitionSide then
|
||
if SoundDebug then
|
||
env.info(string.format("[SND] ToCoalition side=%s (group-only=%s, group=%s)",
|
||
tostring(coalitionSide), tostring(SoundToGroupOnly),
|
||
tostring(canonGroup and canonGroup:GetName() or "nil")))
|
||
end
|
||
soundObj:ToCoalition(coalitionSide)
|
||
return
|
||
end
|
||
|
||
if SoundDebug then env.info("[SND] Play aborted: no valid group and no coalitionSide") end
|
||
end
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- EVENT HANDLERS
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
EventHandler = EVENTHANDLER:New()
|
||
EventHandler:HandleEvent(EVENTS.Shot)
|
||
EventHandler:HandleEvent(EVENTS.Kill)
|
||
EventHandler:HandleEvent(EVENTS.Dead)
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- KILL EVENTS (FULLY MIRRORED, nil-safe)
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
function EventHandler:OnEventKill(EventData)
|
||
if not SoundHandler then return end
|
||
if SoundDebug then
|
||
BASE:I("---------KILL DETECTED----------"); BASE:I(EventData)
|
||
end
|
||
if SoundDebug then LogKillEvent(EventData) end
|
||
|
||
local iniSide = EventData.IniCoalition
|
||
local tgtSide = EventData.TgtCoalition
|
||
local iniGroup = EventData.IniGroup
|
||
local iniGroupName = EventData.IniGroupName
|
||
|
||
-- 1) A2A kill by an airplane → killer hears A2A “good kill”
|
||
local gA2A = iniGroupName and GROUP:FindByName(iniGroupName) or nil
|
||
if (EventData.TgtCategory == 0 or EventData.TgtCategory == 1)
|
||
and EventData.TgtObjectCategory == 1
|
||
and gA2A and gA2A.IsAirPlane and gA2A:IsAirPlane() then
|
||
local s = ChooseRandom(Sounds.Air_Unit_Sound_Table)
|
||
PlayToGroupOrCoalition(NewSound(s), iniGroup, iniSide)
|
||
end
|
||
|
||
-- 2) A2G kill (ground unit killed by airplane) → killer hears ground kill
|
||
local gA2G = iniGroupName and GROUP:FindByName(iniGroupName) or nil
|
||
if gA2G and gA2G.IsAirPlane and gA2G:IsAirPlane()
|
||
and EventData.TgtCategory == 2 and EventData.TgtObjectCategory == 1
|
||
and iniSide ~= tgtSide then
|
||
local s = ChooseRandom(Sounds.Ground_Unit_Sound_Table)
|
||
PlayToGroupOrCoalition(NewSound(s), iniGroup, iniSide)
|
||
end
|
||
|
||
-- 3) Ship killed by airplane → killer hears ground/ship kill
|
||
local gShip = iniGroupName and GROUP:FindByName(iniGroupName) or nil
|
||
if gShip and gShip.IsAirPlane and gShip:IsAirPlane()
|
||
and EventData.TgtCategory == 3 and EventData.TgtObjectCategory == 1 then
|
||
local s = ChooseRandom(Sounds.Ground_Unit_Sound_Table)
|
||
PlayToGroupOrCoalition(NewSound(s), iniGroup, iniSide)
|
||
end
|
||
|
||
-- 4) Friendly aircraft loss: only if a CLIENT aircraft is killed
|
||
if EventData.TgtCategory == 0 and EventData.TgtUnitName then
|
||
local tgtClient = CLIENT:FindByName(EventData.TgtUnitName)
|
||
if tgtClient then
|
||
local s = ChooseRandom(Sounds.FriendlyLosses)
|
||
NewSound(s):ToCoalition(tgtSide)
|
||
end
|
||
end
|
||
|
||
-- 5) Friendly-fire (air vs air): notify the offending side only if target is a CLIENT aircraft
|
||
if iniSide == tgtSide and EventData.TgtCategory == 0 and EventData.TgtUnitName then
|
||
local tgtClient = CLIENT:FindByName(EventData.TgtUnitName)
|
||
if tgtClient then
|
||
local s = ChooseRandom(Sounds.Friendly_Fire_Table)
|
||
PlayToGroupOrCoalition(NewSound(s), iniGroup, iniSide)
|
||
end
|
||
end
|
||
end
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- DEAD EVENTS (MIRRORED)
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
function EventHandler:OnEventDead(EventData)
|
||
if not SoundHandler then return end
|
||
if SoundDebug then
|
||
BASE:I("---------DEAD DETECTED----------"); BASE:I(EventData)
|
||
end
|
||
if SoundDebug then LogDeadUnit(EventData) end
|
||
|
||
-- STATIC DEAD: play to the OPPOSING coalition (keeps the “good hit” vibe)
|
||
if EventData.IniObjectCategory == 3 then
|
||
local s = ChooseRandom(Sounds.Ground_Unit_Sound_Table)
|
||
NewSound(s):ToCoalition(Opposing(EventData.IniCoalition))
|
||
end
|
||
end
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- SHOT EVENTS (mirrored + clarified intent)
|
||
-- - Shooter feedback (own weapon) → Fox/Pickle/Rifle/etc to SHOOTER side (group/coalition)
|
||
-- - Incoming cues:
|
||
-- * Missile fired at a target group → Incoming_Missile_Table to TARGET group
|
||
-- * SAM launch:
|
||
-- - Friendly SAM → Friendly_SAM_Table to shooter side
|
||
-- - Hostile SAM at target → SamSoundTable to TARGET group
|
||
-- - Special cases: Tomahawk to shooter side, SCUD to opposing coalition
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
function EventHandler:OnEventShot(EventData)
|
||
if SoundDebug then
|
||
BASE:I("---------SHOT DETECTED----------"); BASE:I(EventData)
|
||
end
|
||
if not SoundHandler then return end
|
||
if SoundDebug then LogFiringUnit(EventData) end
|
||
if not EventData.Weapon then return end
|
||
|
||
local WeaponDesc = EventData.Weapon:getDesc()
|
||
if not WeaponDesc then return end
|
||
|
||
local iniSide = EventData.IniCoalition
|
||
local tgtGroup = EventData.TgtGroup
|
||
local iniGroup = EventData.IniGroup
|
||
local iniGroupName = EventData.IniGroupName or ""
|
||
local _weapon = EventData.Weapon:getTypeName()
|
||
local category = WeaponDesc.category -- 0 shell, 1 missile, 2 rocket, 3 bomb
|
||
local guidance = WeaponDesc.guidance -- 1 unguided bomb, 2 IR, 3 ARH, 4 SARH, 5 anti-radar, 7 TV/EO
|
||
local missileCat = WeaponDesc.missileCategory
|
||
|
||
local brevity = "none"
|
||
local function pick(tbl) brevity = ChooseRandom(tbl) end
|
||
|
||
local shooterGroup = iniGroupName ~= "" and GROUP:FindByName(iniGroupName) or nil
|
||
|
||
-- Shooter feedback (own-fire), nil-safe airplane check
|
||
if shooterGroup and shooterGroup.IsAirPlane and shooterGroup:IsAirPlane() then
|
||
if _weapon == "AGM_154" or _weapon == "AGM_154A" then
|
||
pick(Sounds.PigsAway_Sound_Table)
|
||
elseif string.find(_weapon, "ADM", 1, true) then
|
||
pick(Sounds.Decoy_Table)
|
||
elseif category == 3 and guidance == 7 then
|
||
pick(Sounds.Paveway_Sound_Table)
|
||
elseif _weapon == "AGM_84D" then
|
||
pick(Sounds.Bruiser_Sound_Table)
|
||
elseif _weapon == "AGM_84H" or string.find(_weapon, "84E", 1, true) then
|
||
pick(Sounds.CruiseMissile_Table)
|
||
elseif category == 1 and guidance == 3 then
|
||
pick(Sounds.Fox3_Sound_Table)
|
||
elseif category == 1 and guidance == 2 then
|
||
pick(Sounds.Fox2_Sound_Table)
|
||
elseif category == 1 and guidance == 4 then
|
||
pick(Sounds.Fox1_Sound_Table)
|
||
elseif category == 1 and guidance == 5 and missileCat == 6 then
|
||
pick(Sounds.Magnum_Sound_Table)
|
||
elseif category == 1 and guidance == 7 or string.find(_weapon, "65", 1, true) then
|
||
pick(Sounds.Rifle_Sound_Table)
|
||
elseif category == 0 then
|
||
pick(Sounds.Rifle_Sound_Table)
|
||
elseif category == 1 then
|
||
pick(Sounds.Rifle_Sound_Table)
|
||
elseif category == 2 then
|
||
pick(Sounds.Rifle_Sound_Table)
|
||
elseif category == 3 then
|
||
if guidance == 1 or _weapon == "GBU_32_V_2B" or string.find(_weapon, "MK", 1, true) or string.find(_weapon, "ROCKEYE", 1, true) then
|
||
pick(Sounds.Pickle_Sound_Table)
|
||
else
|
||
pick(Sounds.Pickle_Sound_Table)
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Friendly SAM cue
|
||
if shooterGroup and shooterGroup.IsSAM and shooterGroup:IsSAM() then
|
||
pick(Sounds.Friendly_SAM_Table)
|
||
end
|
||
|
||
-- Ship-launched Tomahawk cue
|
||
if shooterGroup and shooterGroup.IsShip and shooterGroup:IsShip() then
|
||
if string.find(_weapon, "BGM_109B", 1, true) then
|
||
pick(Sounds.Tomahawk_Table)
|
||
end
|
||
end
|
||
|
||
-- Incoming missile cue to target group
|
||
if tgtGroup and category == 1 then
|
||
local inc = ChooseRandom(Sounds.Incoming_Missile_Table)
|
||
local incsnd = NewSound(inc)
|
||
local function DelayedIncoming() if tgtGroup then incsnd:ToGroup(tgtGroup) end end
|
||
TIMER:New(DelayedIncoming):Start(math.random(3, 7)) --TODO make UI Variables
|
||
end
|
||
|
||
-- Hostile SAM at target → SAM call to TARGET group
|
||
if tgtGroup and shooterGroup and ((shooterGroup.IsSAM and shooterGroup:IsSAM()) or (ShipSamSounds and shooterGroup.IsShip and shooterGroup:IsShip())) and category == 1 then
|
||
if not string.find(_weapon, "SA48N6", 1, true) and not string.find(_weapon, "SCUD_RAKETA", 1, true) then
|
||
local sams = ChooseRandom(Sounds.SamSoundTable)
|
||
local samsnd = NewSound(sams)
|
||
local function DelayedSAMS() if tgtGroup then samsnd:ToGroup(tgtGroup) end end
|
||
TIMER:New(DelayedSAMS):Start(math.random(3, 7))
|
||
end
|
||
end
|
||
|
||
-- Ballistic missile (SCUD) → opposing coalition
|
||
if string.find(_weapon, "SCUD_RAKETA", 1, true) then
|
||
local b = ChooseRandom(Sounds.Ballistic)
|
||
NewSound(b):ToCoalition(Opposing(iniSide))
|
||
end
|
||
|
||
-- Deliver shooter feedback if selected
|
||
if brevity ~= "none" then
|
||
PlayToGroupOrCoalition(NewSound(brevity), iniGroup, iniSide)
|
||
end
|
||
end
|
||
|
||
if SoundDebug then BASE:I("-----MISSILE/BOMB SOUNDS SET------") end
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- SHOOTING (RAPID FIRE) – mirrored & renamed tables
|
||
-- - If a CLIENT fires guns → Guns_OwnFire to their group/coalition
|
||
-- - If guns are being fired at a target group (client) → Guns_Incoming to target group
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
local SHOOT_SFX_COOLDOWN = 30 -- seconds
|
||
local _lastShootSfxAt = -1
|
||
|
||
local function ShootingSfxGate(tag)
|
||
local now = timer.getTime() -- mission time
|
||
if _lastShootSfxAt and _lastShootSfxAt > 0 then
|
||
local dt = now - _lastShootSfxAt
|
||
if dt < SHOOT_SFX_COOLDOWN then
|
||
if SoundDebug then
|
||
env.info(string.format("[SFX-GATE] BLOCK %s: %.1fs remaining", tag,
|
||
SHOOT_SFX_COOLDOWN - dt))
|
||
end
|
||
return false
|
||
end
|
||
end
|
||
_lastShootSfxAt = now
|
||
if SoundDebug then env.info(string.format("[SFX-GATE] ALLOW %s at t=%.1f", tag, now)) end
|
||
return true
|
||
end
|
||
|
||
ShootingEventHandler = EVENTHANDLER:New()
|
||
ShootingEventHandler:HandleEvent(EVENTS.ShootingStart)
|
||
|
||
if SoundDebug then env.info("----- GUN SHOOTING HANDLER INIT (listening for EVENTS.ShootingStart) -----") end
|
||
|
||
local function DBG(...)
|
||
if SoundDebug then
|
||
local msg = table.concat({ ... }, " "); BASE:I(msg); env.info(msg)
|
||
end
|
||
end
|
||
local function BOOLSTR(b) return b and "true" or "false" end
|
||
|
||
local function FindGroupWithLog(name, tag)
|
||
if not name then
|
||
DBG(tag, " : no name provided"); return nil
|
||
end
|
||
local g = GROUP:FindByName(name)
|
||
DBG(tag, " : GROUP:FindByName(", name, ") -> ", tostring(g))
|
||
if g then
|
||
local okExist = g.IsExist and g:IsExist()
|
||
DBG(tag, " : IsExist=", tostring(okExist), " IsAirPlane=", tostring(g.IsAirPlane and g:IsAirPlane() or "n/a"))
|
||
end
|
||
return g
|
||
end
|
||
|
||
local function FindUnitWithLog(dcsUnit, tag)
|
||
if not dcsUnit then
|
||
DBG(tag, " : no DCS unit handle"); return nil
|
||
end
|
||
local u = UNIT:Find(dcsUnit)
|
||
DBG(tag, " : UNIT:Find(DCSUnit) -> ", tostring(u))
|
||
if u then
|
||
DBG(tag, " : IsExist=", BOOLSTR(u.IsExist and u:IsExist()), " IsClient=",
|
||
BOOLSTR(u.IsClient and u:IsClient()))
|
||
end
|
||
return u
|
||
end
|
||
|
||
function ShootingEventHandler:OnEventShootingStart(EventData)
|
||
env.info(string.format("[GUN] ShootingStart fired t=%.2f ini=%s tgt=%s",
|
||
tonumber(EventData.time or -1),
|
||
tostring(EventData.IniUnitName or EventData.IniDCSUnitName or "?"),
|
||
tostring(EventData.TgtUnitName or EventData.TgtDCSUnitName or "?")
|
||
))
|
||
|
||
if SoundDebug then
|
||
DBG("-----RAPID GUNS SHOOTING START-----")
|
||
DBG("EventData fields present:",
|
||
" IniGroupName=", tostring(EventData.IniGroupName),
|
||
" IniUnitName=", tostring(EventData.IniUnitName),
|
||
" IniCoalition=", tostring(EventData.IniCoalition),
|
||
" TgtGroup=", tostring(EventData.TgtGroup),
|
||
" TgtGroupName=", tostring(EventData.TgtGroupName),
|
||
" TgtDCSUnit=", tostring(EventData.TgtDCSUnit),
|
||
" TgtCoalition=", tostring(EventData.TgtCoalition))
|
||
DBG("OneLineSerialize: ", UTILS.OneLineSerialize(EventData))
|
||
end
|
||
|
||
-- Shooter resolution + validation
|
||
local ShooterGroup = FindGroupWithLog(EventData.IniGroupName, "SHOOTER group")
|
||
local ShooterUnit = nil
|
||
if EventData.IniUnitName then
|
||
ShooterUnit = UNIT:FindByName(EventData.IniUnitName)
|
||
DBG("SHOOTER unit : UNIT:FindByName(", EventData.IniUnitName, ") -> ", tostring(ShooterUnit))
|
||
if ShooterUnit then
|
||
DBG("SHOOTER unit : IsExist=", BOOLSTR(ShooterUnit.IsExist and ShooterUnit:IsExist()),
|
||
" IsClient=", BOOLSTR(ShooterUnit.IsClient and ShooterUnit:IsClient()))
|
||
end
|
||
else
|
||
DBG("SHOOTER unit : IniUnitName missing")
|
||
end
|
||
|
||
if not ShooterGroup or not ShooterUnit then
|
||
DBG("ABORT: missing ShooterGroup (", tostring(ShooterGroup), ") or ShooterUnit (", tostring(ShooterUnit), ")")
|
||
return
|
||
end
|
||
|
||
local isPlane = ShooterGroup.IsAirPlane and ShooterGroup:IsAirPlane() or false
|
||
DBG("SHOOTER group type: IsAirPlane=", BOOLSTR(isPlane))
|
||
if not isPlane then
|
||
DBG("ABORT: ShooterGroup is not airplane")
|
||
return
|
||
end
|
||
|
||
-- Own-fire branch (client shooter)
|
||
local shooterIsClient = ShooterUnit.IsClient and ShooterUnit:IsClient() or false
|
||
DBG("SHOOTER IsClient=", BOOLSTR(shooterIsClient), " | PlayOwnShootingGuns=", BOOLSTR(PlayOwnShootingGuns))
|
||
if shooterIsClient and PlayOwnShootingGuns then
|
||
if not ShootingSfxGate("OWNFIRE") then return end
|
||
local s = ChooseRandom(Sounds.Guns_OwnFire)
|
||
DBG("OWNFIRE: choosing sound=", tostring(s))
|
||
local snd = NewSound(s)
|
||
if snd then
|
||
DBG("OWNFIRE: playing to shooter group/coalition (SoundToGroupOnly=", BOOLSTR(SoundToGroupOnly), ")")
|
||
PlayToGroupOrCoalition(snd, EventData.IniGroup, EventData.IniCoalition)
|
||
else
|
||
DBG("OWNFIRE: NewSound failed (nil)")
|
||
end
|
||
return
|
||
elseif shooterIsClient and not PlayOwnShootingGuns then
|
||
DBG("OWNFIRE: suppressed by PlayOwnShootingGuns=false")
|
||
end
|
||
|
||
-- Incoming branch – resolve target group in 3 passes
|
||
local tgtGroup = nil
|
||
if EventData.TgtGroup then
|
||
tgtGroup = EventData.TgtGroup
|
||
DBG("TARGET pass1: EventData.TgtGroup provided -> ", tostring(tgtGroup))
|
||
end
|
||
if not tgtGroup and EventData.TgtGroupName then
|
||
DBG("TARGET pass2: Try TgtGroupName=", tostring(EventData.TgtGroupName))
|
||
tgtGroup = FindGroupWithLog(EventData.TgtGroupName, "TARGET pass2")
|
||
end
|
||
if not tgtGroup and EventData.TgtDCSUnit then
|
||
DBG("TARGET pass3: Try TgtDCSUnit wrapper")
|
||
local u = FindUnitWithLog(EventData.TgtDCSUnit, "TARGET pass3")
|
||
if u and u.IsExist and u:IsExist() then
|
||
tgtGroup = u:GetGroup()
|
||
DBG("TARGET pass3: u:GetGroup() -> ", tostring(tgtGroup))
|
||
if tgtGroup and tgtGroup.IsExist then DBG("TARGET pass3: tgtGroup:IsExist()=", BOOLSTR(tgtGroup:IsExist())) end
|
||
end
|
||
end
|
||
|
||
if not tgtGroup then
|
||
DBG("INCOMING: abort — could not resolve a target group from any field.")
|
||
return
|
||
end
|
||
|
||
-- Strict: confirm target is a client
|
||
local pc = tgtGroup.GetPlayerCount and tgtGroup:GetPlayerCount() or nil
|
||
DBG("CLIENT-CHECK: GetPlayerCount=", tostring(pc))
|
||
local targetIsClient = false
|
||
if pc ~= nil then
|
||
targetIsClient = (pc or 0) > 0
|
||
DBG("CLIENT-CHECK: via GetPlayerCount -> ", BOOLSTR(targetIsClient))
|
||
end
|
||
if pc == nil and tgtGroup.GetPlayerUnits then
|
||
local pus = tgtGroup:GetPlayerUnits()
|
||
local len = (type(pus) == "table") and #pus or 0
|
||
targetIsClient = len > 0
|
||
DBG("CLIENT-CHECK: via GetPlayerUnits (#=", tostring(len), ") -> ", BOOLSTR(targetIsClient))
|
||
end
|
||
if not targetIsClient and EventData.TgtDCSUnit then
|
||
local u = FindUnitWithLog(EventData.TgtDCSUnit, "CLIENT-CHECK fallback")
|
||
targetIsClient = (u and u.IsExist and u:IsExist() and u.IsClient and u:IsClient()) or false
|
||
DBG("CLIENT-CHECK: via UNIT:IsClient fallback -> ", BOOLSTR(targetIsClient))
|
||
end
|
||
|
||
if not targetIsClient then
|
||
DBG("INCOMING: target is NOT a client → no sound (strict mode).")
|
||
return
|
||
end
|
||
|
||
-- Optional: suppress OpFor incoming if disabled
|
||
if EventData.IniCoalition == RED and not PlayOpForShootingGuns then
|
||
DBG("INCOMING: suppressed by PlayOpForShootingGuns=false (OpFor shooter)")
|
||
return
|
||
end
|
||
|
||
-- Cooldown gate for INCOMING
|
||
if not ShootingSfxGate("INCOMING") then return end
|
||
|
||
-- Play incoming to victim client group
|
||
local s = ChooseRandom(Sounds.Guns_Incoming)
|
||
DBG("INCOMING: choosing sound=", tostring(s))
|
||
local snd = NewSound(s)
|
||
if not snd then
|
||
DBG("INCOMING: NewSound failed (nil) for ", tostring(s))
|
||
return
|
||
end
|
||
|
||
-- Canonize tgtGroup before playback
|
||
local gname = tgtGroup.GetName and tgtGroup:GetName() or "?"
|
||
local tgtGroupCanon = GROUP:FindByName(gname) or tgtGroup
|
||
DBG(string.format("INCOMING: resolved tgtGroup name=%s obj=%s", tostring(gname), tostring(tgtGroupCanon)))
|
||
|
||
if tgtGroupCanon and tgtGroupCanon.GetDCSObject then
|
||
local dcs = tgtGroupCanon:GetDCSObject()
|
||
local gid = dcs and dcs:getID() or "?"
|
||
DBG("INCOMING: sending to GroupID=" .. tostring(gid))
|
||
trigger.action.outTextForGroup(gid, "DEBUG: incoming sound " .. tostring(s), 2)
|
||
end
|
||
|
||
PlayToGroupOrCoalition(snd, tgtGroupCanon, nil)
|
||
end
|
||
|
||
if SoundDebug then BASE:I("-----GUN SHOOTING SOUNDS SET (with debug)-----") end
|
||
|
||
-----------------------------------------------------------------
|
||
-- MOOSE CLIENT MENU INTEGRATION (Soundhandler under "Moose Functions")
|
||
-----------------------------------------------------------------
|
||
MooseClientMenus = MooseClientMenus or { bySide = {} }
|
||
|
||
local function _sideNameFromNum(sideNum)
|
||
if sideNum == coalition.side.BLUE then return "blue" end
|
||
if sideNum == coalition.side.RED then return "red" end
|
||
if sideNum == coalition.side.NEUTRAL then return "neutral" end
|
||
return "neutral"
|
||
end
|
||
|
||
-- Ensure a SINGLE visible root "Moose Functions" exists for the coalition.
|
||
local function EnsureClientRootMenu(sideNum)
|
||
local sideName = _sideNameFromNum(sideNum)
|
||
local reg = MooseClientMenus.bySide[sideName]
|
||
if reg and reg.manager and reg.root and reg.root.IsDestroyed ~= true then
|
||
return reg
|
||
end
|
||
|
||
local clientSet = SET_CLIENT:New():FilterCoalitions(sideName):FilterStart()
|
||
local mgr = CLIENTMENUMANAGER:New(clientSet, "Moose Functions")
|
||
local root = mgr:NewEntry("Moose Functions")
|
||
|
||
reg = { manager = mgr, root = root, folders = {}, built = {} }
|
||
MooseClientMenus.bySide[sideName] = reg
|
||
|
||
TIMER:New(function()
|
||
if SoundDebug then BASE:I(("[Soundhandler/ClientMenus] Init for side=%s"):format(sideName)) end
|
||
mgr:Propagate()
|
||
mgr:InitAutoPropagation()
|
||
end):Start(1)
|
||
|
||
return reg
|
||
end
|
||
|
||
-- Make/return a child folder under the visible "Moose Functions" root.
|
||
local function EnsureClientFolder(sideNum, folderName)
|
||
local reg = EnsureClientRootMenu(sideNum)
|
||
local folder = reg.folders[folderName]
|
||
if (not folder) or folder.IsDestroyed == true then
|
||
folder = reg.manager:NewEntry(folderName, reg.root) -- parent = Moose Functions root
|
||
reg.folders[folderName] = folder
|
||
if SoundDebug then BASE:I(("[Soundhandler/ClientMenus] Created folder '%s'"):format(folderName)) end
|
||
end
|
||
return folder, reg.manager, reg
|
||
end
|
||
|
||
-----------------------------------------------------------------
|
||
-- SOUNDHANDLER MENUS under Moose Functions
|
||
-----------------------------------------------------------------
|
||
local function SoundhandlerOnOff(boolean, _Group, _Client)
|
||
SoundHandler = boolean
|
||
env.info("-----SoundHandler On = " .. tostring(boolean) .. "-----")
|
||
trigger.action.outText(boolean and "**Soundhandler ON**" or "--Soundhandler OFF--", 5)
|
||
end
|
||
|
||
local function SwitchDebug(boolean, _Group, _Client)
|
||
SoundDebug = boolean
|
||
env.info("-----SoundDebug Now = " .. tostring(boolean) .. "-----")
|
||
end
|
||
|
||
local function SwitchSoundsToGroup(boolean, _Group, _Client)
|
||
SoundToGroupOnly = boolean
|
||
env.info("-----SoundToGroupOnly Now = " .. tostring(boolean) .. "-----")
|
||
if not boolean then trigger.action.outText("--Sounds Play To All--", 5) end
|
||
end
|
||
|
||
local function BuildSoundhandlerMenus(sideNum)
|
||
local root, mgr, reg = EnsureClientFolder(sideNum, "Soundhandler")
|
||
if reg.built["Soundhandler"] then
|
||
if SoundDebug then BASE:I("[Soundhandler/ClientMenus] Already built for this side, skipping") end
|
||
return mgr
|
||
end
|
||
|
||
local onOffFolder = mgr:NewEntry("On or Off", root)
|
||
local debugFolder = mgr:NewEntry("Sound Debug", root)
|
||
local scopeFolder = mgr:NewEntry("Sounds To Group or All", root)
|
||
|
||
mgr:NewEntry("On", onOffFolder, SoundhandlerOnOff, true, "nil")
|
||
mgr:NewEntry("Off", onOffFolder, SoundhandlerOnOff, false, "nil")
|
||
|
||
mgr:NewEntry("On", debugFolder, SwitchDebug, true, "nil")
|
||
mgr:NewEntry("Off", debugFolder, SwitchDebug, false, "nil")
|
||
|
||
mgr:NewEntry("Play Sounds to Group Only", scopeFolder, SwitchSoundsToGroup, true, "nil")
|
||
mgr:NewEntry("Play Sounds to All", scopeFolder, SwitchSoundsToGroup, false, "nil")
|
||
|
||
reg.built["Soundhandler"] = true
|
||
if SoundDebug then BASE:I("[Soundhandler/ClientMenus] Added entries under Moose Functions > Soundhandler") end
|
||
return mgr
|
||
end
|
||
|
||
BuildSoundhandlerMenus(coalition.side.BLUE)
|
||
BuildSoundhandlerMenus(coalition.side.RED)
|
||
-- BuildSoundhandlerMenus(coalition.side.NEUTRAL) -- if ever needed
|
||
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
-- LOGGING UTILITIES
|
||
-----------------------------------------------------------------------------------------------------------------------------------
|
||
function LogFiringUnit(EventData)
|
||
local coalitionStr = "UNKNOWN"
|
||
if EventData.IniCoalition == 1 then
|
||
coalitionStr = "RED"
|
||
elseif EventData.IniCoalition == 2 then
|
||
coalitionStr = "BLUE"
|
||
elseif EventData.IniCoalition == 0 then
|
||
coalitionStr = "NEUTRAL"
|
||
end
|
||
|
||
local groupType = "UNIT"
|
||
if EventData.IniGroupName then
|
||
local group = GROUP:FindByName(EventData.IniGroupName)
|
||
if group then
|
||
if group:IsAirPlane() then
|
||
groupType = "AIRPLANE"
|
||
elseif group:IsHelicopter() then
|
||
groupType = "HELICOPTER"
|
||
elseif group:IsGround() then
|
||
groupType = "GROUND UNIT"
|
||
elseif group:IsShip() then
|
||
groupType = "SHIP"
|
||
end
|
||
end
|
||
end
|
||
|
||
local groupName = EventData.IniGroupName or "UNKNOWN GROUP"
|
||
BASE:I(string.format("------[SHOT] %s %s (%s)", coalitionStr, groupType, groupName))
|
||
end
|
||
|
||
function LogDeadUnit(EventData)
|
||
local coalitionStr = "UNKNOWN"
|
||
if EventData.IniCoalition == 1 then
|
||
coalitionStr = "RED"
|
||
elseif EventData.IniCoalition == 2 then
|
||
coalitionStr = "BLUE"
|
||
elseif EventData.IniCoalition == 0 then
|
||
coalitionStr = "NEUTRAL"
|
||
end
|
||
|
||
local groupType = "UNIT"
|
||
if EventData.IniGroupName then
|
||
local group = GROUP:FindByName(EventData.IniGroupName)
|
||
if group then
|
||
if group:IsAirPlane() then
|
||
groupType = "AIRPLANE"
|
||
elseif group:IsHelicopter() then
|
||
groupType = "HELICOPTER"
|
||
elseif group:IsGround() then
|
||
groupType = "GROUND UNIT"
|
||
elseif group:IsShip() then
|
||
groupType = "SHIP"
|
||
end
|
||
end
|
||
end
|
||
|
||
local groupName = EventData.IniGroupName or "UNKNOWN GROUP"
|
||
BASE:I(string.format("------[DEAD] %s %s (%s)", coalitionStr, groupType, groupName))
|
||
end
|
||
|
||
function LogKillEvent(EventData)
|
||
local iniUnitName = EventData.IniUnitName or "Unknown Shooter"
|
||
local iniGroupName = EventData.IniGroupName or "Unknown Group"
|
||
local tgtUnitName = EventData.TgtUnitName or "Unknown Target"
|
||
local tgtGroupName = EventData.TgtGroupName or "Unknown Target Group"
|
||
|
||
local iniCoalitionStr = "UNKNOWN"
|
||
if EventData.IniCoalition == 1 then
|
||
iniCoalitionStr = "RED"
|
||
elseif EventData.IniCoalition == 2 then
|
||
iniCoalitionStr = "BLUE"
|
||
elseif EventData.IniCoalition == 0 then
|
||
iniCoalitionStr = "NEUTRAL"
|
||
end
|
||
|
||
local tgtCoalitionStr = "UNKNOWN"
|
||
if EventData.TgtCoalition == 1 then
|
||
tgtCoalitionStr = "RED"
|
||
elseif EventData.TgtCoalition == 2 then
|
||
tgtCoalitionStr = "BLUE"
|
||
elseif EventData.TgtCoalition == 0 then
|
||
tgtCoalitionStr = "NEUTRAL"
|
||
end
|
||
|
||
local iniGroupType = "UNIT"
|
||
local shooterGroup = GROUP:FindByName(iniGroupName)
|
||
if shooterGroup then
|
||
if shooterGroup:IsAirPlane() then
|
||
iniGroupType = "AIRPLANE"
|
||
elseif shooterGroup:IsHelicopter() then
|
||
iniGroupType = "HELICOPTER"
|
||
elseif shooterGroup:IsGround() then
|
||
iniGroupType = "GROUND UNIT"
|
||
elseif shooterGroup:IsShip() then
|
||
iniGroupType = "SHIP"
|
||
end
|
||
end
|
||
|
||
local tgtGroupType = "UNIT"
|
||
local targetGroup = GROUP:FindByName(tgtGroupName)
|
||
if targetGroup then
|
||
if targetGroup:IsAirPlane() then
|
||
tgtGroupType = "AIRPLANE"
|
||
elseif targetGroup:IsHelicopter() then
|
||
tgtGroupType = "HELICOPTER"
|
||
elseif targetGroup:IsGround() then
|
||
tgtGroupType = "GROUND UNIT"
|
||
elseif targetGroup:IsShip() then
|
||
tgtGroupType = "SHIP"
|
||
end
|
||
end
|
||
|
||
BASE:I(string.format(
|
||
"-----[UNIT KILL] %s %s (%s: %s) killed %s %s (%s: %s)",
|
||
iniCoalitionStr, iniGroupType, iniGroupName, iniUnitName,
|
||
tgtCoalitionStr, tgtGroupType, tgtGroupName, tgtUnitName
|
||
))
|
||
end
|
||
|
||
env.info("-----DCSRetribution|MOOSE Soundhandler plugin - configuration end -----")
|