792 lines
34 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 -----")