From edf70e971cd8769ee3a8b705012378caf4adcdff Mon Sep 17 00:00:00 2001 From: Geoff Franks Date: Wed, 10 Jun 2026 21:02:40 -0400 Subject: [PATCH 01/11] fix(Spawn): air-spawn takeoff event never fired (wrong arg packing) SPAWN:SpawnAtAirbase scheduled the synthetic takeoff event for air-spawned groups with: self:ScheduleOnce(5, BASE.CreateEventTakeoff, {GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()}) BASE:ScheduleOnce(Start, SchedulerFunction, ...) forwards its trailing arguments to the scheduled function as varargs (it wraps `...` in a table internally). Wrapping the arguments in a table here made that table argument number one, so CreateEventTakeoff ran with the {group, time, dcs} table as `self`. A plain table has no :F(), the very first line errored, and the S_EVENT_TAKEOFF event was never raised. Air-spawned AI_A2A_DISPATCHER GCI defenders therefore never activated (observed: zero QRA scrambled with takeoff = Air). The sibling call site in SpawnAtParkingSpot is correct because it uses SCHEDULER:New(nil, fn, {args}, 5), which DOES take a single argument table. Drop the braces so the args pass as varargs, matching CreateEventTakeoff's (self=group, EventTime, Initiator) signature. Co-Authored-By: Claude Opus 4.8 --- Moose Development/Moose/Core/Spawn.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index 3e5ffea8a..cfbbd79b8 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -2430,8 +2430,13 @@ function SPAWN:SpawnAtAirbase( SpawnAirbase, Takeoff, TakeoffAltitude, TerminalT -- When spawned in the air, we need to generate a Takeoff Event. if Takeoff == GROUP.Takeoff.Air then for UnitID, UnitSpawned in pairs( GroupSpawned:GetUnits() ) do - --SCHEDULER:New( nil, BASE.CreateEventTakeoff, { GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject() }, 5 ) --No need to create a new SCHEDULER instance every time! - self:ScheduleOnce(5, BASE.CreateEventTakeoff, {GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()}) + -- BASE:ScheduleOnce forwards its trailing args to the scheduled function as + -- varargs, so they must NOT be wrapped in a table here (unlike SCHEDULER:New, + -- which takes a single argument table). Passing a table made it argument #1, + -- i.e. CreateEventTakeoff ran with that table as `self`, so the takeoff event + -- never fired and air-spawned groups (e.g. AI_A2A_DISPATCHER GCI defenders) + -- never activated. + self:ScheduleOnce(5, BASE.CreateEventTakeoff, GroupSpawned, timer.getTime(), UnitSpawned:GetDCSObject()) end end From b6ea6606fc8976a93b9a2345611bb708ebd70e33 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 21 Jun 2026 12:51:50 +0200 Subject: [PATCH 02/11] #SCHEDULEDISPATCHER - Fixing a memory leak. Thanks to @Shazzer1951 --- .../Moose/Core/ScheduleDispatcher.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 7f0102943..0d6fa6df5 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -246,11 +246,11 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr -- self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else - self:Stop( Scheduler, CallID ) + self:_Reclaim( Scheduler, CallID ) end else - self:Stop( Scheduler, CallID ) + self:_Reclaim( Scheduler, CallID ) end else self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) @@ -375,3 +375,13 @@ function SCHEDULEDISPATCHER:NoTrace( Scheduler ) Scheduler.ShowTrace = false end +--- Helper for memory cleanup for self stopping schedulers +-- @param #SCHEDULEDISPATCHER self +-- @param Core.Scheduler#SCHEDULER Scheduler Scheduler object. +-- @param #string CallID (Optional) Scheduler Call ID. +function SCHEDULEDISPATCHER:_Reclaim( Scheduler, CallID ) + self:Stop( Scheduler, CallID ) -- remove DCS timer, nil ScheduleID + if self.Schedule[Scheduler] then self.Schedule[Scheduler][CallID] = nil end + self.ObjectSchedulers[CallID] = nil + self.PersistentSchedulers[CallID] = nil +end From 7e9dd9deff401d5badd0a1cc9fc605951dd107f5 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 23 Jun 2026 12:29:45 +0200 Subject: [PATCH 03/11] #EASYGCICAP - fall back to GCI behaviour if no CAP Points have been defined. --- Moose Development/Moose/Ops/EasyGCICAP.lua | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Ops/EasyGCICAP.lua b/Moose Development/Moose/Ops/EasyGCICAP.lua index 1b2afb44b..8166e5d8e 100644 --- a/Moose Development/Moose/Ops/EasyGCICAP.lua +++ b/Moose Development/Moose/Ops/EasyGCICAP.lua @@ -135,6 +135,7 @@ -- local mywing = EASYGCICAP:New("Blue CAP Operations",AIRBASE.Caucasus.Kutaisi,"blue","Blue EWR") -- -- -- Add a CAP patrol point belonging to our airbase, we'll be at 30k ft doing 400 kn, initial direction 90 degrees (East), leg 20NM +-- -- NOTE - Skip this function and do not create CAP Points if you want GCI behaviour only. -- mywing:AddPatrolPointCAP(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone 1"):GetCoordinate(),30000,400,90,20) -- -- -- Add a Squadron with template "Blue Sq1 M2000c", 20 airframes, skill good, Modex starting with 102 and skin "Vendee Jeanne" @@ -286,7 +287,7 @@ EASYGCICAP = { --- EASYGCICAP class version. -- @field #string version -EASYGCICAP.version="0.1.36" +EASYGCICAP.version="0.1.37" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- @@ -754,6 +755,15 @@ end function EASYGCICAP:_AddAirwing(Airbasename, Alias) self:T(self.lid.."_AddAirwing "..Airbasename) + -- Gather Some Stats + local function counttable(tbl) + local count = 0 + for _,_data in pairs(tbl) do + count = count + 1 + end + return count + end + local CapFormation = self.CapFormation local DespawnAfterLanding = self.DespawnAfterLanding local DespawnAfterHolding = self.DespawnAfterHolding @@ -772,7 +782,11 @@ function EASYGCICAP:_AddAirwing(Airbasename, Alias) CAP_Wing:SetMarker(false) CAP_Wing:SetAirbase(AIRBASE:FindByName(Airbasename)) CAP_Wing:SetRespawnAfterDestroyed() - CAP_Wing:SetNumberCAP(self.capgrouping) + + --- #DONE avoid wings with no CAP points starting CAP anyhow; AirWing uses this to start CAP and creates points when there are none. + if counttable(self.ManagedCP) >0 then + CAP_Wing:SetNumberCAP(self.capgrouping) + end CAP_Wing:SetCapCloseRaceTrack(true) if self.showpatrolpointmarks then @@ -1039,7 +1053,7 @@ function EASYGCICAP:_SetCAPPatrolPoints() MESSAGE:New(self.lid.."You are trying to create a CAP point for which there is no wing! "..tostring(data.AirbaseName),30,"CHECK"):ToAllIf(self.debug):ToLog() return end - local Wing = self.wings[data.AirbaseName][1] -- Ops.Airwing#AIRWING + local Wing = self.wings[data.AirbaseName][1] -- Ops.AirWing#AIRWING local Coordinate = data.Coordinate local Altitude = data.Altitude local Speed = data.Speed From f769a0deb55e4b9bce3dc0fd4b4bbd248d8d5663 Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:56:31 +0200 Subject: [PATCH 04/11] Update Utils.lua Added KA-50_3 door check. --- Moose Development/Moose/Utilities/Utils.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index b75fe8935..d4e933658 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -2378,6 +2378,11 @@ function UTILS.IsLoadingDoorOpen( unit_name ) BASE:T(unit_name .. " front door(s) are open or doors removed") return true end + + if type_name == "Ka-50_3" and unit:getDrawArgumentValue(38) == 1 then + BASE:T(unit_name .. " cockpit door is open") + return true + end if type_name=="C-130J-30" and (unit:getDrawArgumentValue(86)==1)then BASE:T(unit_name.." rear doors are open") From 8c60a8c678954592cb4f780acdfeec6e031e33a2 Mon Sep 17 00:00:00 2001 From: Thomas <72444570+Applevangelist@users.noreply.github.com> Date: Fri, 26 Jun 2026 12:31:54 +0200 Subject: [PATCH 05/11] Update ScheduleDispatcher.lua Undoing previous change as some delayed event calls stopped working, probably FSM. Need to find the reason for that. --- Moose Development/Moose/Core/ScheduleDispatcher.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Core/ScheduleDispatcher.lua b/Moose Development/Moose/Core/ScheduleDispatcher.lua index 0d6fa6df5..05fe6d25f 100644 --- a/Moose Development/Moose/Core/ScheduleDispatcher.lua +++ b/Moose Development/Moose/Core/ScheduleDispatcher.lua @@ -246,11 +246,11 @@ function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleAr -- self:T3( { Repeat = CallID, CurrentTime, ScheduleTime, ScheduleArguments } ) return ScheduleTime -- returns the next time the function needs to be called. else - self:_Reclaim( Scheduler, CallID ) + self:Stop( Scheduler, CallID ) end else - self:_Reclaim( Scheduler, CallID ) + self:Stop( Scheduler, CallID ) end else self:I( "<<<>" .. Name .. ":" .. Line .. " (" .. Source .. ")" ) From 626b12f486db981dadd71721044cb9f530c2eb9a Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sat, 27 Jun 2026 10:57:27 +0200 Subject: [PATCH 06/11] #MSRS #HOUND corrected frequency formatting to allow 1000ths of a frequency --- Moose Development/Moose/Sound/SRS.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index d048bc844..d69a703f7 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -2108,7 +2108,7 @@ function MSRS:_HoundTextToSpeech(Message,Frequencies,Modulations,Volume,Label,Co local ffs = {} for _,_f in pairs(Frequencies) do - table.insert(ffs,string.format("%.1f",_f)) + table.insert(ffs,string.format("%.3f",_f)) end local freqs = table.concat(ffs, ",") @@ -2214,7 +2214,7 @@ function MSRS:_HoundTestTone(Frequencies, Modulations, Coalition) local ffs = {} for _,_f in pairs(Frequencies or self.frequencies) do - table.insert(ffs,string.format("%.1f",_f)) + table.insert(ffs,string.format("%.3f",_f)) end local freqs = table.concat(ffs, ",") @@ -2291,7 +2291,7 @@ function MSRS:RadioJammerOn(Frequencies, Modulations, Coalition, Noisetype, Volu local ffs = {} for _,_f in pairs(Frequencies or self.frequencies) do - table.insert(ffs,string.format("%.1f",_f)) + table.insert(ffs,string.format("%.3f",_f)) end local freqs = table.concat(ffs, ",") From 98ed7d50c19343a2409304a44de55d2b3ff63ca2 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Sun, 28 Jun 2026 09:30:45 +0200 Subject: [PATCH 07/11] #EASYGCICAP * Added `SetDefaultINTERCEPTAlt()` --- Moose Development/Moose/Ops/EasyGCICAP.lua | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Ops/EasyGCICAP.lua b/Moose Development/Moose/Ops/EasyGCICAP.lua index 8166e5d8e..d1d499ddb 100644 --- a/Moose Development/Moose/Ops/EasyGCICAP.lua +++ b/Moose Development/Moose/Ops/EasyGCICAP.lua @@ -191,9 +191,10 @@ -- * @{#EASYGCICAP.SetDefaultResurrection}: Set how many seconds the AirWing stays inoperable after the AirWing STATIC HQ ist destroyed, default 900 secs. -- * @{#EASYGCICAP.SetDefaultCAPSpeed}: Set how many knots the CAP flights should do (will be altitude corrected), default 300 kn. -- * @{#EASYGCICAP.SetDefaultCAPAlt}: Set at which altitude (ASL) the CAP planes will fly, default 25,000 ft. +-- * @{#EASYGCICAP.SetDefaultINTERCEPTAlt}: Set at which altitude (ASL) the Intercept planes will fly, default 25,000 ft. -- * @{#EASYGCICAP.SetDefaultCAPDirection}: Set the initial direction from the CAP point the planes will fly in degrees, default is 90°. -- * @{#EASYGCICAP.SetDefaultCAPLeg}: Set the length of the CAP leg, default is 15 NM. --- * @{#EASYGCICAP.SetDefaultCAPGrouping}: Set how many planes will be spawned per mission (CVAP/GCI), defaults to 2. +-- * @{#EASYGCICAP.SetDefaultCAPGrouping}: Set how many planes will be spawned per mission (CAP/GCI), defaults to 2. -- * @{#EASYGCICAP.SetDefaultMissionRange}: Set how many NM the planes can go from the home base, defaults to 100. -- * @{#EASYGCICAP.SetDefaultNumberAlert5Standby}: Set how many planes will be spawned on cold standby (Alert5), default 2. -- * @{#EASYGCICAP.SetDefaultEngageRange}: Set max engage range for CAP flights if they detect intruders, defaults to 50. @@ -287,7 +288,7 @@ EASYGCICAP = { --- EASYGCICAP class version. -- @field #string version -EASYGCICAP.version="0.1.37" +EASYGCICAP.version="0.1.38" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- @@ -588,6 +589,17 @@ function EASYGCICAP:SetDefaultCAPAlt(Altitude) return self end +--- Set default INTERCEPT Altitude in feet +-- @param #EASYGCICAP self +-- @param #number Altitude (Optional) Altitude defaults to 25000 +-- @return #EASYGCICAP self +function EASYGCICAP:SetDefaultINTERCEPTAlt(Altitude) + self:T(self.lid.."SetDefaultINTERCEPTAlt") + self.interceptalt = Altitude or 25000 + return self +end +-- + --- Set default CAP lieg initial direction in degrees -- @param #EASYGCICAP self -- @param #number Direction (Optional) Direction defaults to 90 (East) @@ -1519,6 +1531,7 @@ function EASYGCICAP:_AssignIntercept(Cluster) local overhead = self.overhead local capspeed = self.capspeed + 100 local capalt = self.capalt + local interalt = self.interceptalt or self.capalt local maxsize = self.maxinterceptsize local repeatsonfailure = self.repeatsonfailure @@ -1606,6 +1619,7 @@ function EASYGCICAP:_AssignIntercept(Cluster) :SetRepeatOnFailure(repeats) :SetMissionSpeed(UTILS.KnotsToAltKIAS(capspeed,capalt)) :SetMissionAltitude(capalt) + :SetEngageAltitude(interalt) if nogozoneset:Count() > 0 then InterceptAuftrag:AddConditionSuccess( From af4f20af67542755f1c63440b0f107670b65dfcf Mon Sep 17 00:00:00 2001 From: leka1986 <83298840+leka1986@users.noreply.github.com> Date: Sun, 12 Jul 2026 13:30:33 +0200 Subject: [PATCH 08/11] Fix aircraft model names for Lancer and Kiowa --- Moose Development/Moose/Utilities/Enums.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 62fe643d7..2ca2f6c79 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -523,7 +523,7 @@ ENUMS.ReportingName = Tornado = "Tornado", -- Transport / Bomber / Others Atlas = "A400", - Lancer = "B1-B", + Lancer = "B-1B", Stratofortress = "B-52H", Herc = "C-130", Hercules = "C-130J-30", @@ -568,7 +568,7 @@ ENUMS.ReportingName = Apache = "AH-64", Chinook = "CH-47", Sea_Stallion = "CH-53", - Kiowa = "OH-58", + Kiowa = "OH58D", Seahawk = "SH-60", Blackhawk = "UH-60", Sea_King = "S-61", From 3d79a16d3a831021d802d30fe1973a53de93205d Mon Sep 17 00:00:00 2001 From: Shafik Date: Mon, 13 Jul 2026 11:02:42 +0300 Subject: [PATCH 09/11] [FIX] "Parameter #1 (group id) missed" refreshing menus for dead groups --- Moose Development/Moose/Ops/CTLD.lua | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 2b7bc20fa..28e06c80e 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -6171,11 +6171,11 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult local ctype = Build.Type -- #CTLD_CARGO.Enum local canmove = false if ctype == CTLD_CARGO.Enum.VEHICLE then canmove = true end - if ctype == CTLD_CARGO.Enum.STATIC then - return self + if ctype == CTLD_CARGO.Enum.STATIC then + return self end local temptable = Build.Template or {} - if type(temptable) == "string" then + if type(temptable) == "string" then temptable = {temptable} end local zone = nil -- Core.Zone#ZONE_RADIUS @@ -6217,8 +6217,11 @@ function CTLD:_BuildObjectFromCrates(Group,Unit,Build,Repair,RepairLocation,Mult self:__CratesBuild(1,Group,Unit,self.DroppedTroops[self.TroopCounter]) end end -- template loop - self:_RefreshLoadCratesMenu(Group, Unit) - self:_RefreshPackMenus(Group, Unit) + + if Group and Group:IsAlive() and Group:GetID() then + self:_RefreshLoadCratesMenu(Group, Unit) + self:_RefreshPackMenus(Group, Unit) + end else self:T(self.lid.."Group KIA while building!") end From 338ec34604ba55cf441284d2b7ea9f4bc407867e Mon Sep 17 00:00:00 2001 From: patpatpowercat <56987247+patpatpowercat@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:47:47 -0700 Subject: [PATCH 10/11] Update Mantis.lua Added ships to MANTIS. NAVYGROUPs may be integrated. Need to be aware MANTIS doesn't change ROE. SEAD does not (by default) effect ships. Ships are individually identified, not as group. Group will react to jamming as a whole, will go live/sleep based upon longest range/best radar. No longer need prefixes for either ground or naval units. --- Moose Development/Moose/Functional/Mantis.lua | 908 +++++++++++++++++- 1 file changed, 858 insertions(+), 50 deletions(-) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 91f728ac3..858b9e302 100644 --- a/Moose Development/Moose/Functional/Mantis.lua +++ b/Moose Development/Moose/Functional/Mantis.lua @@ -524,6 +524,137 @@ MANTIS.SamDataCH = { ["Lvkv9040M CHM"] = { Range=2, Blindspot=0.1, Height=1.2, Type="Point", Radar="LvKv9040",Point="true" }, } + +--- @type MANTIS.SamDataNaval +MANTIS.SamDataNaval = { + --------------------------------------------------------------------- + -- CurrentHill (CH) surface combatants + --------------------------------------------------------------------- + -- USA -- AEGIS / SM-family, S-band (SPY-1) or S-band AESA (SPY-6) + ["Ticonderoga"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Ticonderoga" }, + ["Ticonderoga CMP"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Ticonderoga_CMP" }, + ["Arleigh Burke IIA"] = { Range=150, Blindspot=4, Height=33, Type="Long", Radar="CH_Arleigh_Burke_IIA" }, + ["Arleigh Burke III"] = { Range=180, Blindspot=3, Height=33, Type="Long", Radar="CH_Arleigh_Burke_III" }, + ["Constellation"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="CH_Constellation" }, + + -- UK + ["Type 45"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="Type45" }, -- Aster 15/30, SAMPSON + ["Type 26"] = { Range=25, Blindspot=1, Height=10, Type="Medium", Radar="CH_Type26" }, -- Sea Ceptor / CAMM + + -- Germany + ["F124 Sachsen"] = { Range=120, Blindspot=3, Height=24, Type="Long", Radar="CH_F124" }, -- SM-2/ESSM, APAR (X-band) + + -- China + ["Type 052D"] = { Range=100, Blindspot=5, Height=27, Type="Long", Radar="Type052D" }, -- HHQ-9 + ["Type 055"] = { Range=150, Blindspot=5, Height=30, Type="Long", Radar="Type055" }, -- HHQ-9B + ["Type 054B"] = { Range=70, Blindspot=2, Height=20, Type="Medium", Radar="CH_Type054B" }, -- HHQ-16 + ["Type 056A"] = { Range=9, Blindspot=0.3, Height=6, Type="Point", Radar="CH_Type056A", Point="true" }, -- HHQ-10 (IR) + + -- Russia + ["Admiral Gorshkov"] = { Range=150, Blindspot=3, Height=30, Type="Long", Radar="CH_Admiral_Gorshkov" }, -- Poliment-Redut 9M96D + ["Grigorovich AShM"] = { Range=50, Blindspot=2, Height=15, Type="Medium", Radar="CH_Grigorovich_AShM" }, -- Shtil-1 + ["Grigorovich LACM"] = { Range=50, Blindspot=2, Height=15, Type="Medium", Radar="CH_Grigorovich_LACM" }, + ["Steregushchiy"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Steregushchiy" }, -- Redut 9M96 + ["Gremyashchiy AShM"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Gremyashchiy_AShM" }, + ["Gremyashchiy LACM"] = { Range=50, Blindspot=1.5, Height=20, Type="Medium", Radar="CH_Gremyashchiy_LACM" }, + ["Karakurt AShM"] = { Range=20, Blindspot=1, Height=15, Type="Point", Radar="CH_Karakurt_AShM", Point="true" }, -- Pantsir-M + ["Karakurt LACM"] = { Range=20, Blindspot=1, Height=15, Type="Point", Radar="CH_Karakurt_LACM", Point="true" }, + + -- Point-defense / MANPADS-only hulls (treated as close-in; also usable as EWR pickets) + ["Type 022 FAC"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_Type022", Point="true" }, + ["IRGCN FAC AShM"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_IranFAC_MG", Point="true" }, -- MANPADS + ["IRGCN FAC Igla"] = { Range=5, Blindspot=0.2, Height=4, Type="Point", Radar="CH_IranFAC_MG_AShM", Point="true" }, -- Igla + ["Strb 90 FAC"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="Strb90", Point="true" }, + ["Visby"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="HSwMS_Visby", Point="true" }, -- gun/CIWS + + --------------------------------------------------------------------- + -- Base-game DCS surface combatants (type ids from pydcs / DCS export) + -- Only air-defence-capable hulls are listed. Pure-gun / no-AD hulls + -- (Leander gun refits, La Combattante, cargo, subs, WWII) are omitted + -- on purpose; add them as POINT if you want them to appear at all. + --------------------------------------------------------------------- + -- USA + ["CG Ticonderoga"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="TICONDEROG" }, -- SM-2/SPY-1 NOTE: id has NO trailing 'E' + ["DDG Arleigh Burke IIa"] = { Range=120, Blindspot=3, Height=30, Type="Long", Radar="USS_Arleigh_Burke_IIa" }, -- SM-2/SPY-1D + ["FFG Perry"] = { Range=38, Blindspot=2, Height=15, Type="Medium", Radar="PERRY" }, -- SM-1MR (est. real range) + ["CVN Roosevelt"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_71", Point="true" }, -- NSSM/RAM + ["CVN Lincoln"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_72", Point="true" }, + ["CVN Washington"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_73", Point="true" }, + ["CVN Stennis"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="Stennis", Point="true" }, + ["CVN Truman"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="CVN_75", Point="true" }, + ["CVN Vinson"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="VINSON", Point="true" }, + ["LHA Tarawa"] = { Range=15, Blindspot=1, Height=8, Type="Point", Radar="LHA_Tarawa", Point="true" }, -- RAM/Sea Sparrow + -- Russia + ["Cruiser Moskva"] = { Range=75, Blindspot=5, Height=25, Type="Long", Radar="MOSCOW" }, -- S-300F (SA-N-6) + ["BC Pyotr Velikiy"] = { Range=150, Blindspot=5, Height=27, Type="Long", Radar="PIOTR" }, -- S-300F/FM + ["Frigate Neustrashimy"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="NEUSTRASH" }, -- Kinzhal (SA-N-9) + ["Frigate Rezky"] = { Range=15, Blindspot=1, Height=5, Type="Short", Radar="REZKY" }, -- OSA-M (SA-N-4) + ["Corvette Grisha"] = { Range=15, Blindspot=1, Height=5, Type="Short", Radar="ALBATROS" }, -- OSA-M (SA-N-4) + ["Corvette Molniya"] = { Range=5, Blindspot=0.3, Height=3, Type="Point", Radar="MOLNIYA", Point="true" }, -- Strela + ["CV Kuznetsov"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="KUZNECOW" }, -- Kinzhal/Kashtan + ["CV Kuznetsov 2017"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="CV_1143_5" }, + ["Patrol Bykov TorM2KM"] = { Range=15, Blindspot=1, Height=6, Type="Short", Radar="CHAP_Project22160_TorM2KM" }, -- Tor + ["Patrol Bykov"] = { Range=7, Blindspot=0.3, Height=4, Type="Point", Radar="CHAP_Project22160", Point="true" }, + -- China + ["Type 052C"] = { Range=100, Blindspot=5, Height=27, Type="Long", Radar="Type_052C" }, -- HHQ-9 + ["Type 052B"] = { Range=30, Blindspot=2, Height=15, Type="Medium", Radar="Type_052B" }, -- HHQ-16/Shtil + ["Type 054A"] = { Range=45, Blindspot=2, Height=18, Type="Medium", Radar="Type_054A" }, -- HHQ-16 + ["Type 071 LPD"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="Type_071", Point="true" }, -- HHQ-10 + -- UK + ["HMS Invincible"] = { Range=40, Blindspot=2, Height=18, Type="Medium", Radar="hms_invincible" }, -- Sea Dart + + --------------------------------------------------------------------- + -- desdemicabina-simulation 3.2 (Spanish/Australian navies) + -- AEGIS hulls: two ship_MK41_SM2 launchers (SM-2, 64 cells) + AEGIS-class + -- trackers (150km/30km alt, ECM_K 0.5). NOTE: this pack's airWeaponDist=45km + -- UNDERSTATES the fit -- Range below follows the SM-2 launcher (~100km), the + -- inverse of the Admiral pack's inflated values. + --------------------------------------------------------------------- + ["HMAS Hobart DDG-39"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="DDG39" }, -- AEGIS SPY-1D(V), SM-2 + ["F100 Alvaro de Bazan"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="F100" }, -- AEGIS SPY-1D, SM-2 + ["F105 Cristobal Colon"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="F105" }, -- AEGIS SPY-1D, SM-2 + ["LHD Canberra L02"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="L02", Point="true" }, -- 3x Phalanx only + ["LPD Castilla L52"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="L52", Point="true" }, -- 2x Phalanx (NO_SAM attr) + ["LHD Juan Carlos I L61"] = { Range=1.5, Blindspot=0.1, Height=0.8, Type="Point", Radar="L61", Point="true" }, -- .50cal mounts only + + --------------------------------------------------------------------- + -- Admiral mod surface combatants + -- Worked example (USS America LHA-6). Read each field from the ship's + -- GT def file: + -- Radar <- GT.Name (the typeName; the essential, non-guessable key) + -- Range <- GT.airWeaponDist / 1000 (20000 -> 20) + -- Height <- launcher max_trg_alt / 1000 (15000 -> 15) + -- Blindspot <- launcher distanceMin / 1000 (400 -> 0.4) + -- Type <- from armament fit (Sea Sparrow => SHORT) + --------------------------------------------------------------------- + ["USS America LHA-6"] = { Range=20, Blindspot=0.4, Height=15, Type="Short", Radar="USS America LHA-6" }, -- Sea Sparrow + RAM + Phalanx + + -- IMPORTANT: these Range values come from the actual weapon launcher's distanceMax + -- / the real system, NOT GT.airWeaponDist (which is unreliable here -- e.g. Blue + -- Ridge and Henry Kaiser both claim 100-150km with only CIWS/MGs). + -- LONG -- area SAM + ["HMS Duncan (Type 45)"] = { Range=100, Blindspot=3, Height=30, Type="Long", Radar="HMS Duncan" }, -- Aster/Sea Viper (mod: MK41_SM2) + ["Aquitaine D650 (FREMM)"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="Aquitaine D650" }, -- SYLVER Aster 30 (distanceMax 120km) + ["Normandie D651 (FREMM)"] = { Range=120, Blindspot=3, Height=27, Type="Long", Radar="D651_Normandie" }, -- SYLVER Aster 30 + -- MEDIUM + ["MEKO-200HN Hydra"] = { Range=50, Blindspot=2, Height=20, Type="Medium", Radar="MEKO_HN" }, -- MK41 (mod SM-2 / real ESSM) + ["MEKO-200TN Barbaros"] = { Range=50, Blindspot=2, Height=20, Type="Medium", Radar="MEKO_TN" }, -- MK41 + SeaZenith CIWS + ["Sovremenny Adm. Ushakov"] = { Range=46, Blindspot=2, Height=15, Type="Medium", Radar="Admiral Ushakov" }, -- SA-N-7 Gadfly + -- SHORT + ["Udaloy II Chabanenko"] = { Range=12, Blindspot=1, Height=6, Type="Short", Radar="Udaloy II DDG Admiral Chabanenko" }, -- SA-N-9 Klinok + ["USS San Antonio LPD-17"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="USS San Antonio LPD-17" }, -- Sea Sparrow + -- POINT -- CIWS / RAM / gun / IR only (no area SAM); unjammable in the jammer table + ["USS Oak Hill LSD-51"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="USS_Oak_Hill LSD-51", Point="true" }, -- RAM + Phalanx + ["USS Blue Ridge LCC-19"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="USS_Blue_Ridge", Point="true" }, -- Phalanx only + ["Sacramento AOE-1"] = { Range=9, Blindspot=0.5, Height=6, Type="Point", Radar="USS_Support_Ship_Sacramento_AOE-1", Point="true" }, -- RAM + Phalanx + ["Henry Kaiser T-AO-187"] = { Range=1.5, Blindspot=0.1, Height=0.8,Type="Point", Radar="USNS_Henry_Kaiser", Point="true" }, -- .50cal only (negligible) + ["HMS Albion L14"] = { Range=3, Blindspot=0.1, Height=2, Type="Point", Radar="HMS_Albion L14", Point="true" }, -- Phalanx only + ["LPD Foudre"] = { Range=6, Blindspot=0.3, Height=4, Type="Point", Radar="LPD_Foundre", Point="true" }, -- Mistral (IR) + ["Osa-I Missile Boat"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="OSA_One", Point="true" }, -- AK-230 (NO_SAM) + ["Osa-II Missile Boat"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="OSA_Two", Point="true" }, -- AK-230 + ["IRIS Alvand"] = { Range=4, Blindspot=0.2, Height=3, Type="Point", Radar="IRIS_Alvand", Point="true" }, -- OtoBreda 40mm / 20mm +} + ----------------------------------------------------------------------- -- MANTIS Jammer Extension v2.0.0 -- Standoff Jamming (SOJ) aircraft support for MANTIS IADS networks @@ -705,6 +836,119 @@ do end +--- @type MANTIS.JammerSAMParamsNaval +MANTIS.JammerSAMParamsNaval = { + -- Calibration note (research pass): high power-aperture warship radars + -- (SPY-1/SPY-6/SAMPSON/Poliment/HHQ-9B) are modeled HARDER to jam than their + -- ground peers -- lower peak/floor, mu pushed outward -- reflecting shipboard + -- prime power, large multi-face arrays, low sidelobes and sensor redundancy. + -- Documented ECM-weak legacy sets (SPG-51 CW illum, Type 909) slightly easier. + -- Navalized twins remain anchored to ground counterparts (Shtil=SA-11, + -- Klinok=Tor, OSA-M=SA-8, S-300F=SA-10). Open sources support ordering, not + -- exact percentages -- tune in-mission as needed. + -- USA AEGIS + ["CH_Ticonderoga"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 }, + ["CH_Ticonderoga_CMP"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 }, + ["CH_Arleigh_Burke_IIA"] = { peak=17, mu=70, sigma_L=24, tail_dist=115, band="S", floor=2 }, + ["CH_Arleigh_Burke_III"] = { peak=14, mu=83, sigma_L=28, tail_dist=130, band="S", floor=2 }, -- SPY-6 AESA + ["CH_Constellation"] = { peak=14, mu=63, sigma_L=22, tail_dist=100, band="S", floor=2 }, -- SPY-6(V)3 + -- UK + ["Type45"] = { peak=16, mu=62, sigma_L=24, tail_dist=110, band="S", floor=2 }, -- SAMPSON + ["CH_Type26"] = { peak=18, mu=18, sigma_L=7, tail_dist=40, band="IJ", floor=2 }, -- CAMM active RH + -- Germany + ["CH_F124"] = { peak=18, mu=62, sigma_L=24, tail_dist=110, band="IJ", floor=2 }, -- APAR X-band + -- China + ["Type052D"] = { peak=20, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, + ["Type055"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, + ["CH_Type054B"] = { peak=22, mu=38, sigma_L=16, tail_dist=75, band="S", floor=3 }, + ["CH_Type056A"] = { peak=8, mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0 }, -- HHQ-10 IR + -- Russia + ["CH_Admiral_Gorshkov"] = { peak=16, mu=80, sigma_L=30, tail_dist=145, band="S", floor=2 }, -- Poliment AESA + ["CH_Grigorovich_AShM"] = { peak=50, mu=28, sigma_L=12, tail_dist=58, band="IJ", floor=3 }, -- Shtil-1 + ["CH_Grigorovich_LACM"] = { peak=50, mu=28, sigma_L=12, tail_dist=58, band="IJ", floor=3 }, + ["CH_Steregushchiy"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, -- Redut + ["CH_Gremyashchiy_AShM"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, + ["CH_Gremyashchiy_LACM"] = { peak=22, mu=35, sigma_L=15, tail_dist=70, band="S", floor=2 }, + ["CH_Karakurt_AShM"] = { peak=10, mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0 }, -- Pantsir-M + ["CH_Karakurt_LACM"] = { peak=10, mu=9, sigma_L=4, tail_dist=20, band="IJ", floor=0 }, + -- CIWS / MANPADS-only hulls -> optical, essentially unjammable (floor 0) + ["CH_Type022"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["CH_IranFAC_MG"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["CH_IranFAC_MG_AShM"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["Strb90"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, + ["HSwMS_Visby"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, -- gun/CIWS: unjammable + + -- Base-game DCS hulls (canonical entries; aliases set in the do-block below) + ["TICONDEROG"] = { peak=17, mu=60, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1 (NO trailing E) + ["USS_Arleigh_Burke_IIa"] = { peak=17, mu=60, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D + ["PERRY"] = { peak=33, mu=20, sigma_L=9, tail_dist=48, band="IJ", floor=3 }, -- SM-1 / SPG-51 (X-band illum) + ["MOSCOW"] = { peak=32, mu=42, sigma_L=18, tail_dist=90, band="S", floor=3 }, -- S-300F + ["PIOTR"] = { peak=28, mu=70, sigma_L=26, tail_dist=130, band="S", floor=3 }, -- S-300F/FM + ["NEUSTRASH"] = { peak=28, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=2 }, -- Kinzhal + ["REZKY"] = { peak=35, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=3 }, -- OSA-M + ["MOLNIYA"] = { peak=3, mu=3, sigma_L=1, tail_dist=5, band="OPT",floor=0 }, -- Strela + ["KUZNECOW"] = { peak=24, mu=8, sigma_L=3.5,tail_dist=18, band="IJ", floor=2 }, -- Kinzhal/Kashtan + ["CHAP_Project22160_TorM2KM"] = { peak=28, mu=12, sigma_L=5, tail_dist=28, band="IJ", floor=3 }, -- Tor M2KM + ["Type_052C"] = { peak=20, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- HHQ-9 + ["Type_052B"] = { peak=30, mu=20, sigma_L=10, tail_dist=48, band="S", floor=3 }, -- HHQ-16/Shtil + ["Type_054A"] = { peak=26, mu=28, sigma_L=12, tail_dist=58, band="S", floor=3 }, -- HHQ-16 + ["hms_invincible"] = { peak=36, mu=25, sigma_L=11, tail_dist=52, band="IJ", floor=3 }, -- Sea Dart / Type 909 + ["CVN_71"] = { peak=8, mu=5, sigma_L=2, tail_dist=12, band="OPT",floor=0 }, -- NSSM/RAM (carrier point) + + -- Admiral mod -- worked example (USS America LHA-6): + -- band <- radar band (Sea Sparrow Mk 95 illuminator = X-band => IJ) + -- floor <- inverse of GT ECM_K hint (ECM_K 0.65 = moderate => floor ~2-3) + -- mu <- engagement range in nm (20 km ~= 11 nm) + -- peak/sigma_L/tail_dist <- copy nearest short-IJ analog (Sea Sparrow ~ NASAMS/SA-11 class) + ["USS America LHA-6"] = { peak=22, mu=11, sigma_L=5, tail_dist=25, band="IJ", floor=3 }, + -- desdemicabina AEGIS hulls: SPY-1 family fingerprint (peak 17 / floor 2 / S), + -- mu scaled to the ~100km SM-2 envelope of these units. + ["DDG39"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D(V) + ["F100"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D + ["F105"] = { peak=17, mu=55, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- SPY-1D + -- Admiral SAM shooters (POINT hulls are aliased to the CIWS OPT entry in the do-block below) + ["HMS Duncan"] = { peak=16, mu=54, sigma_L=22, tail_dist=110, band="S", floor=2 }, -- Aster (ECM_K 0.65) + ["Aquitaine D650"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, -- Aster 30 (mod ECM_K 5.0 - see note) + ["D651_Normandie"] = { peak=16, mu=65, sigma_L=24, tail_dist=120, band="S", floor=2 }, -- Aster 30 + ["MEKO_HN"] = { peak=22, mu=27, sigma_L=12, tail_dist=55, band="IJ", floor=2 }, -- ESSM/SM-2 + ["MEKO_TN"] = { peak=22, mu=27, sigma_L=12, tail_dist=55, band="IJ", floor=2 }, + ["Admiral Ushakov"] = { peak=48, mu=25, sigma_L=11, tail_dist=55, band="IJ", floor=3 }, -- SA-N-7 Gadfly + ["Udaloy II DDG Admiral Chabanenko"] = { peak=28, mu=7, sigma_L=3, tail_dist=18, band="IJ", floor=3 }, -- SA-N-9 Klinok + ["USS San Antonio LPD-17"] = { peak=22, mu=8, sigma_L=4, tail_dist=20, band="IJ", floor=3 }, -- Sea Sparrow + ["LPD_Foundre"] = { peak=10, mu=3, sigma_L=2, tail_dist=10, band="OPT",floor=0 }, -- Mistral (IR, near-unjammable) +} + +-- Naval jammer aliases: variants that share a hull/radar point to one curve, +-- so tuning the canonical entry updates all of them (mirrors JammerSAMParams). +do + local n = MANTIS.JammerSAMParamsNaval + -- US carriers + LHA + Type 071: point self-defence (NSSM/RAM/HHQ-10), optical-grade + n["CVN_72"] = n["CVN_71"]; n["CVN_73"] = n["CVN_71"]; n["CVN_75"] = n["CVN_71"] + n["Stennis"] = n["CVN_71"]; n["VINSON"] = n["CVN_71"]; n["LHA_Tarawa"] = n["CVN_71"] + n["Type_071"] = n["CVN_71"] + -- Grisha shares the SA-N-4 (OSA-M) curve with Rezky + n["ALBATROS"] = n["REZKY"] + -- Kuznetsov 2017 variant + n["CV_1143_5"] = n["KUZNECOW"] + -- Vasily Bykov base (gun) ~ Strela-grade optical + n["CHAP_Project22160"] = n["MOLNIYA"] + -- Admiral mod CIWS / RAM / gun-only hulls: all optical-grade, essentially unjammable. + -- Aliased to the base OPT curve so they resolve without cluttering the table. + n["USS_Oak_Hill LSD-51"] = n["MOLNIYA"] -- RAM + Phalanx + n["USS_Blue_Ridge"] = n["MOLNIYA"] -- Phalanx + n["USS_Support_Ship_Sacramento_AOE-1"] = n["MOLNIYA"] -- RAM + Phalanx + n["USNS_Henry_Kaiser"] = n["MOLNIYA"] -- .50cal + n["HMS_Albion L14"] = n["MOLNIYA"] -- Phalanx + n["OSA_One"] = n["MOLNIYA"] -- AK-230 (NO_SAM) + n["OSA_Two"] = n["MOLNIYA"] -- AK-230 + n["IRIS_Alvand"] = n["MOLNIYA"] -- OtoBreda 40mm / 20mm + -- desdemicabina amphibs: CIWS / gun-only, optical-grade unjammable + n["L02"] = n["MOLNIYA"] -- 3x Phalanx + n["L52"] = n["MOLNIYA"] -- 2x Phalanx (NO_SAM) + n["L61"] = n["MOLNIYA"] -- .50cal only +end + + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -741,6 +985,8 @@ do -- mybluemantis:Start() -- function MANTIS:New(name,samprefix,ewrprefix,hq,Coalition,dynamic,awacs, EmOnOff, Padding, Zones) + -- Inject NAVYGROUP:SetMANTIS convenience API (safe if NAVYGROUP not loaded/already done) + MANTIS._InjectNavyGroupAPI() -- Inherit everything from BASE class. @@ -771,6 +1017,12 @@ do self.grouping = 5000 self.acceptrange = 80000 self.detectinterval = 30 + self.SEADNaval = false -- ships hard-kill inbound missiles; they do not go EMCON under SEAD (see SetNavalSEAD) + self.NavalSurfaceWakeup = true -- quiet ships go active when enemy surface combatants close (see SetNavalSurfaceWakeup) + self.NavalSurfaceWakeupRadius = nil -- meters; nil = each ship uses its own engagement radius + self.NavalPerUnit = true -- classify & switch every ship unit individually (see SetNavalPerUnit) + self.NavalAutonomy = true -- ship groups outside EWR radar coverage go alarm RED and search autonomously (see SetNavalAutonomy) + self.NavalAutonomyRadius = 150000 -- meters; a ship group within this distance of an alive EWR group counts as covered self.engagerange = 95 self.autorelocate = false self.autorelocateunits = { HQ = false, EWR = false} @@ -1086,6 +1338,21 @@ do end local function SwitchSAMOn(Name,Group) + if self.NavalPerUnit and Group and Group:IsShip() and self._navalSAMs then + -- Per-unit naval: a hit wakes this group's managed radars individually + if not self.SuppressedGroups[Name] then + for _,_unit in pairs(Group:GetUnits() or {}) do + local uname = _unit:GetName() + if self._navalSAMs[uname] and self.SamStateTracker[uname] == "GREEN" + and not (self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[uname]) then + self.SamStateTracker[uname] = "RED" + end + end + self:_ApplyNavalAlarmState() -- apply group alarm state immediately on hit + self:__RedState(1,Group) + end + return + end local suppressed = self.SuppressedGroups[Name] or false local jammed = self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[Name] or false if not suppressed and not jammed and self.SamStateTracker[Name] == "GREEN" then @@ -1790,6 +2057,7 @@ do local dist = 0 local include = true if grp:IsGround() then include = false end + if grp:IsShip() then include = false end -- MANTIS is anti-air: surface contacts are not engageable targets if grp:GetCoalition() == self.coalition then include = false end if coord and SamCoordinate and grp:IsHelicopter() then dist = coord:Get2DDistance(SamCoordinate) or 0 @@ -1853,7 +2121,29 @@ do if targetdistance <= rad then inrange = "true" end - local text = string.format("Checking SAM at %s | Tgtdist %.1fkm | Rad %.1fkm | Inrange %s", samstring, targetdistance/1000, rad/1000, inrange) + local text = string.format("Checking SAM at %s | Tgtdist %.1fkm | Rad %.1fkm | Inrange %s | Zonecheck %s", samstring, targetdistance/1000, rad/1000, inrange, tostring(zonecheck)) + -- Naval per-unit state summary + if self._navalSAMs and next(self._navalSAMs) ~= nil then + local ntot, nred, njam = 0, 0, 0 + local detail = "" + for uname,_ in pairs(self._navalSAMs) do + local u = UNIT:FindByName(uname) + if u and u:IsAlive() then + ntot = ntot + 1 + local st = self.SamStateTracker[uname] or "GREEN" + if st == "RED" then nred = nred + 1 end + local jm = "" + if self._jammedSAMs and self._jammedSAMs[uname] then njam = njam + 1 jm = " (JAMMED)" end + local pk = "" + local parent = self._navalUnitParent and self._navalUnitParent[uname] + if parent and self._navalAutonomous and self._navalAutonomous[parent] then pk = " [AUTONOMOUS]" end + if self.verbose then detail = detail..string.format("\n %s: %s%s%s",uname,st,jm,pk) end + end + end + if ntot > 0 then + text = text..string.format("\nNaval units: %d | RED %d | GREEN %d | jammed %d",ntot,nred,ntot-nred,njam)..detail + end + end local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug) self:T(self.lid..text) end @@ -1988,37 +2278,66 @@ do local blind = 0 local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP local units = group:GetUnits() - local SAMData = self.SamData local ARMCapacity + -- Ordered multi-table search: the mod-tagged table (if any) first, then all + -- others. This lets groups classify by radar TYPE NAME alone, so ground SAM + -- groups no longer need the designation (or even the mod tag) in their name. + local SearchTables if mod then - SAMData = self.SamDataHDS + SearchTables = { {self.SamDataHDS,"HDS"}, {self.SamData,nil}, {self.SamDataSMA,"SMA"}, {self.SamDataCH,"CHM"} } elseif sma then - SAMData = self.SamDataSMA + SearchTables = { {self.SamDataSMA,"SMA"}, {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataCH,"CHM"} } elseif chm then - SAMData = self.SamDataCH + SearchTables = { {self.SamDataCH,"CHM"}, {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataSMA,"SMA"} } + else + SearchTables = { {self.SamData,nil}, {self.SamDataHDS,"HDS"}, {self.SamDataSMA,"SMA"}, {self.SamDataCH,"CHM"} } end self:T("Looking to auto-match for "..grpname) for _,_unit in pairs(units) do local unit = _unit -- Wrapper.Unit#UNIT local typename = string.lower(unit:GetTypeName()) self:T(string.format("Matching typename: %s",typename)) - for idx,entry in pairs(SAMData) do - local _entry = entry -- #MANTIS.SamData - local _radar = string.lower(_entry.Radar) - self:T(string.format("Trying typename: %s",_radar)) - if string.find(typename,_radar,1,true) then - type = _entry.Type - radiusscale = self.radiusscale[type] - range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on - height = _entry.Height * 1000 -- max firing height - blind = _entry.Blindspot * 100 -- blind spot range - ARMCapacity = _entry.ARMCapacity - self:T(string.format("Match: %s - %s",_radar,type)) - found = true - break + -- Longest-match wins across ALL tables. This prevents a short Radar key in + -- one table (e.g. base "TorM2") from shadowing a more specific key in + -- another (e.g. CH "TorM2K"), and makes the typename path deterministic + -- (plain pairs() order no longer decides between overlapping keys). + -- Table order (mod-tagged table first) breaks exact-length ties. + local bestentry, bestidx, besttag, bestlen = nil, nil, nil, 0 + for _,searchentry in ipairs(SearchTables) do + local SAMData, modtag = searchentry[1], searchentry[2] + if SAMData then + for idx,entry in pairs(SAMData) do + local _radar = string.lower(entry.Radar) + if #_radar > bestlen and string.find(typename,_radar,1,true) then + bestentry, bestidx, besttag, bestlen = entry, idx, modtag, #_radar + end + end end end - if found then break end + if bestentry then + local _entry = bestentry -- #MANTIS.SamData + type = _entry.Type + radiusscale = self.radiusscale[type] + range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on + height = _entry.Height * 1000 -- max firing height + blind = _entry.Blindspot * 100 -- blind spot range + ARMCapacity = _entry.ARMCapacity + self:T(string.format("Match: %s - %s",string.lower(_entry.Radar),type)) + -- Stash the jammer curve by DESIGNATION (table key), so groups named + -- without a designation remain jammable. Reuses _ResolveJammerParams + -- (aliases, digit-boundary matching, mod bucketing) via a synthetic name. + self._samJammerParams = self._samJammerParams or {} + if self._samJammerParams[grpname] == nil then + local synth = bestidx + if besttag and not string.find(bestidx, besttag, 1, true) then + synth = bestidx .. " " .. besttag + end + local jp = self:_ResolveJammerParams(synth) + if jp then self._samJammerParams[grpname] = jp end + end + found = true + break + end end --- AAA or Point Defense if not found then @@ -2037,6 +2356,188 @@ do return range, height, type, blind, ARMCapacity end + --- [Internal] Function to get SAM firing data for naval (ship) groups. + -- Matches the unit type name exactly against #MANTIS.SamDataNaval and, as a side + -- effect, stashes the hull's jammer curve in self._samJammerParams[grpname] so the + -- SOJ jammer extension can suppress it by type name (not group name). + -- @param #MANTIS self + -- @param #string grpname Name of the ship group + -- @return #number range Max firing range (m) + -- @return #number height Max firing height (m) + -- @return #string type #MANTIS.SamType + -- @return #number blind Blind spot + -- @return #number ARMCapacity + function MANTIS:_GetNavalSAMData(grpname) + self:T(self.lid.."_GetNavalSAMData for "..tostring(grpname)) + self._navalSAMs = self._navalSAMs or {} + self._navalSAMs[grpname] = true + self._samJammerParams = self._samJammerParams or {} + local range = self.checkradius + local height = 3000 + local type = MANTIS.SamType.POINT + local blind = 0 + local ARMCapacity = 0 + local group = GROUP:FindByName(grpname) + if not group then + self._samJammerParams[grpname] = nil + return range, height, type, blind, ARMCapacity + end + local units = group:GetUnits() or {} + for _,_unit in pairs(units) do + local typename = string.lower(_unit:GetTypeName()) + for _,entry in pairs(self.SamDataNaval) do + if typename == string.lower(entry.Radar) then + type = entry.Type + local radiusscale = self.radiusscale[type] or 1 + range = entry.Range * 1000 * radiusscale + height = entry.Height * 1000 + blind = entry.Blindspot + ARMCapacity = entry.ARMCapacity or 0 + self._samJammerParams[grpname] = self.JammerSAMParamsNaval[entry.Radar] + self:T(string.format("Naval match: %s -> %s (range %sm)", typename, tostring(type), tostring(range))) + return range, height, type, blind, ARMCapacity + end + end + end + -- Unmatched ship: conservative point-defence default (mirrors AAA fallback), no jammer curve. + self._samJammerParams[grpname] = nil + self:E(self.lid..string.format("*****Could not match naval radar data for %s! Defaulting to POINT.",grpname)) + return 2000, 2000, MANTIS.SamType.POINT, 50, 0 + end + + --- [Internal] Classify a naval group PER UNIT. Each hull matching #MANTIS.SamDataNaval + -- becomes its own SAM-table record keyed by UNIT name, record[8] = parent group name. + -- @param #MANTIS self + -- @return #boolean handled True if at least one unit was classified. + function MANTIS:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) + self:T(self.lid.."_BuildNavalUnitEntries for "..tostring(grpname)) + self._navalSAMs = self._navalSAMs or {} + self._samJammerParams = self._samJammerParams or {} + local entries = 0 + local seadadded = false + local units = group:GetUnits() or {} + for _,_unit in pairs(units) do + if _unit and _unit:IsAlive() then + local typename = string.lower(_unit:GetTypeName()) + for _,entry in pairs(self.SamDataNaval) do + if typename == string.lower(entry.Radar) then + local unitname = _unit:GetName() + local type = entry.Type + local radiusscale = self.radiusscale[type] or 1 + local range = entry.Range * 1000 * radiusscale + local height = entry.Height * 1000 + local blind = entry.Blindspot + local coord = _unit:GetCoordinate() + local record = {unitname, coord, range, height, blind, type, nil, grpname} + table.insert( SAM_Tbl, record ) + if type == MANTIS.SamType.LONG then table.insert( SAM_Tbl_lg, record ) + elseif type == MANTIS.SamType.MEDIUM then table.insert( SAM_Tbl_md, record ) + elseif type == MANTIS.SamType.SHORT then table.insert( SAM_Tbl_sh, record ) + elseif type == MANTIS.SamType.POINT then table.insert( SAM_Tbl_pt, record ) end + self._navalSAMs[unitname] = true + self._navalUnitParent = self._navalUnitParent or {} + self._navalUnitParent[unitname] = grpname + self._samJammerParams[unitname] = self.JammerSAMParamsNaval[entry.Radar] + if self.SamStateTracker[unitname] == nil then self.SamStateTracker[unitname] = "GREEN" end + if self.SEADNaval and not seadadded then + table.insert( SEAD_Grps, grpname ) -- SEAD keys on GROUP names; add once + seadadded = true + end + entries = entries + 1 + self:T(string.format("Naval per-unit match: %s (%s) -> %s",unitname,typename,tostring(type))) + break + end + end + end + end + return entries > 0 + end + + --- [Internal] Group ALARM-STATE arbiter for per-unit naval control. + -- DCS reliably honors only GROUP-level alarm state on ships, so per-unit + -- tracking decides intent and this function applies ONE alarm state per ship + -- group as the final word of each cycle: + -- RED if any managed unit is RED and not jammed (threat / hit / surface + -- wake), or the group is outside EWR radar coverage (autonomous + -- search, see SetNavalAutonomy) with at least one unjammed unit; + -- GREEN otherwise -- including full jamming suppression, which overrides + -- autonomy (a fully jammed group cannot search). + -- @param #MANTIS self + -- @return #MANTIS self + function MANTIS:_ApplyNavalAlarmState() + if not (self.NavalPerUnit and self._navalUnitParent) then return self end + local wants, seen, unjammed = {}, {}, {} + for uname, grpname in pairs(self._navalUnitParent) do + local u = UNIT:FindByName(uname) + if u and u:IsAlive() then + seen[grpname] = true + local jammed = self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[uname] + if not jammed then unjammed[grpname] = true end + if self.SamStateTracker[uname] == "RED" and not jammed then + wants[grpname] = true + end + end + end + -- coverage sources: every alive EWR group (incl. EWR-role ships and AWACS ships in the EWR set) + local sources = {} + if self.NavalAutonomy then + -- Managed naval SAM groups cannot be their own coverage: when GREEN their + -- radars are dark, so a "BOTH"-role group must not self-license through + -- its EWR-set membership. Only UNMANAGED emitters count as sources: + -- land EWR, AWACS, and EWR-only ships (never state-switched, always + -- radiating). + local managed = {} + for _, gname in pairs(self._navalUnitParent) do managed[gname] = true end + self.EWR_Group:ForEachGroupAlive( + function(grp) + if not managed[grp:GetName()] then + local c = grp:GetCoordinate() + if c then table.insert(sources, c) end + end + end) + end + self._navalAutonomous = self._navalAutonomous or {} + self._navalGroupState = self._navalGroupState or {} + for grpname,_ in pairs(seen) do + local grp = GROUP:FindByName(grpname) + if grp and grp:IsAlive() then + local red = wants[grpname] or false + local autonomous = false + if self.NavalAutonomy and unjammed[grpname] and not red then + local covered = false + local gc = grp:GetCoordinate() + if gc then + for _,c in ipairs(sources) do + if c:Get2DDistance(gc) <= self.NavalAutonomyRadius then + covered = true + break + end + end + end + self:T(self.lid..string.format("%s EWR coverage: %s (%d unmanaged source(s), radius %dkm)",grpname,tostring(covered),#sources,self.NavalAutonomyRadius/1000)) + if not covered then + red = true + autonomous = true + end + end + self._navalAutonomous[grpname] = autonomous or nil + local newstate = red and "RED" or "GREEN" + if self._navalGroupState[grpname] ~= newstate then + self._navalGroupState[grpname] = newstate + if red then + grp:OptionAlarmStateRed() + self:T(self.lid..grpname.." naval group -> alarm RED"..(autonomous and " (autonomous: no EWR coverage)" or "")) + else + grp:OptionAlarmStateGreen() + self:T(self.lid..grpname.." naval group -> alarm GREEN") + end + end + end + end + return self + end + + --- [Internal] Function to get SAM firing data -- @param #MANTIS self -- @param #string grpname Name of the group @@ -2046,6 +2547,14 @@ do -- @return #number blind "blind" spot function MANTIS:_GetSAMRange(grpname) self:T(self.lid.."_GetSAMRange for "..tostring(grpname)) + -- Naval hulls: match by exact unit type name against SamDataNaval and stash the + -- jammer curve; bypass the ground name-matching below. + do + local grp = GROUP:FindByName(grpname) + if grp and grp:IsShip() then + return self:_GetNavalSAMData(grpname) + end + end local range = self.checkradius local height = 3000 local type = MANTIS.SamType.MEDIUM @@ -2122,10 +2631,16 @@ do local engagerange = self.engagerange -- firing range in % of max --cycle through groups and set alarm state etc for _i,_group in pairs (SAM_Grps) do - if _group:IsGround() and _group:IsAlive() then + if (_group:IsGround() or _group:IsShip()) and _group:IsAlive() then local group = _group -- Wrapper.Group#GROUP -- DONE: add emissions on/off - if self.UseEmOnOff then + if group:IsShip() and self.NavalPerUnit then + -- Per-unit naval control: ships are switched by GROUP ALARM STATE only, + -- the one mechanism DCS reliably honors on naval groups. Baseline is + -- GREEN; the alarm arbiter (_ApplyNavalAlarmState) raises the group RED + -- on threat, hit, surface contact, or loss of EWR coverage. + group:OptionAlarmStateGreen() + elseif self.UseEmOnOff then group:OptionAlarmStateRed() group:EnableEmission(false) --group:SetAIOff() @@ -2135,30 +2650,47 @@ do group:OptionEngageRange(engagerange) --default engagement will be 95% of firing range local grpname = group:GetName() local grpcoord = group:GetCoordinate() + if group:IsShip() and self.NavalPerUnit + and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then + self:T(grpname.." handled as per-unit naval group") + else local grprange,grpheight,type,blind,ARMCapacity = self:_GetSAMRange(grpname) if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity}) --table.insert( SEAD_Grps, grpname ) if type == MANTIS.SamType.LONG then table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type}) - table.insert( SEAD_Grps, grpname ) + if (not group:IsShip()) or self.SEADNaval then + table.insert( SEAD_Grps, grpname ) + end self:T("SAM "..grpname.." is type LONG") elseif type == MANTIS.SamType.MEDIUM then table.insert( SAM_Tbl_md, {grpname, grpcoord, grprange, grpheight, blind, type}) - table.insert( SEAD_Grps, grpname ) + if (not group:IsShip()) or self.SEADNaval then + table.insert( SEAD_Grps, grpname ) + end self:T("SAM "..grpname.." is type MEDIUM") elseif type == MANTIS.SamType.SHORT then table.insert( SAM_Tbl_sh, {grpname, grpcoord, grprange, grpheight, blind, type}) - table.insert( SEAD_Grps, grpname ) + if (not group:IsShip()) or self.SEADNaval then + table.insert( SEAD_Grps, grpname ) + end self:T("SAM "..grpname.." is type SHORT") elseif type == MANTIS.SamType.POINT then table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T("SAM "..grpname.." is type POINT") - self.ShoradGroupSet:Add(grpname,group) - if not self.autoshorad then - table.insert( SEAD_Grps, grpname ) - end + if group:IsShip() then + if self.SEADNaval then + table.insert( SEAD_Grps, grpname ) -- naval point-defence -> SEAD only when opted in; never SHORAD scoot + end + else + self.ShoradGroupSet:Add(grpname,group) + if not self.autoshorad then + table.insert( SEAD_Grps, grpname ) + end + end end + end -- else: per-unit naval group handled above self.SamStateTracker[grpname] = "GREEN" end end @@ -2198,16 +2730,20 @@ do for _i,_group in pairs (SAM_Grps) do local group = _group -- Wrapper.Group#GROUP group:OptionEngageRange(engagerange) --engagement will be 95% of firing range - if group:IsGround() and group:IsAlive() then + if (group:IsGround() or group:IsShip()) and group:IsAlive() then local grpname = group:GetName() local grpcoord = group:GetCoordinate() + if group:IsShip() and self.NavalPerUnit + and self:_BuildNavalUnitEntries(group, grpname, SAM_Tbl, SAM_Tbl_lg, SAM_Tbl_md, SAM_Tbl_sh, SAM_Tbl_pt, SEAD_Grps) then + self:T(grpname.." handled as per-unit naval group") + else local grprange, grpheight,type,blind, ARMCapacity = self:_GetSAMRange(grpname) -- TODO the below might stop working at some point after some hours, needs testing --local radaralive = group:IsSAM() if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end local radaralive = true table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity}) -- make the table lighter, as I don't really use the zone here - if type ~= MANTIS.SamType.POINT then + if type ~= MANTIS.SamType.POINT and ((not group:IsShip()) or self.SEADNaval) then table.insert( SEAD_Grps, grpname ) end if type == MANTIS.SamType.LONG and radaralive then @@ -2222,11 +2758,18 @@ do elseif type == MANTIS.SamType.POINT or (not radaralive) then table.insert( SAM_Tbl_pt, {grpname, grpcoord, grprange, grpheight, blind, type}) self:T({grpname,grprange, grpheight}) - self.ShoradGroupSet:Add(grpname,group) - if self.autoshorad then - self.Shorad.Groupset = self.ShoradGroupSet + if group:IsShip() then + if self.SEADNaval then + table.insert( SEAD_Grps, grpname ) -- naval point-defence -> SEAD only when opted in; never SHORAD scoot + end + else + self.ShoradGroupSet:Add(grpname,group) + if self.autoshorad then + self.Shorad.Groupset = self.ShoradGroupSet + end end end + end -- else: per-unit naval group handled above end end self.SAM_Table = SAM_Tbl @@ -2414,6 +2957,7 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea for _,_data in pairs (samset) do local samcoordinate = _data[2] local name = _data[1] + local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name local radius = _data[3] local height = _data[4] local blind = _data[5] * 1.25 + 1 @@ -2421,19 +2965,44 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea if not shortsam then shortsam = (_data[6] == MANTIS.SamType.POINT) and true or false end - local samgroup = GROUP:FindByName(name) + local samunit = nil + local samgroup = nil + if navalparent then + samunit = UNIT:FindByName(name) + samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent) + else + samgroup = GROUP:FindByName(name) + end + local samalive = false + if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false + elseif samgroup then samalive = samgroup:IsAlive() or false end local IsInZone, Distance = self:_CheckObjectInZone(detset, samcoordinate, radius, height, dlink) - local suppressed = self.SuppressedGroups[name] or false + -- Naval surface wake-up: quiet ships activate when enemy surface combatants + -- close within the trigger radius. Jamming has precedence and holds them down. + if (not IsInZone) and self.NavalSurfaceWakeup and self._navalSAMs and self._navalSAMs[name] + and not (self._jammerEnabled and self._jammedSAMs and self._jammedSAMs[name]) then + local wakeradius = self.NavalSurfaceWakeupRadius or radius + if self:_EnemySurfaceInRange(samcoordinate, wakeradius) then + IsInZone = true + Distance = wakeradius -- keep distance-based proximity behaviors (smoke, SHORAD link) inert + end + end + local suppressed = self.SuppressedGroups[name] or (navalparent and self.SuppressedGroups[navalparent]) or false local activeshorad = false if self.Shorad and self.Shorad.ActiveGroups and self.Shorad.ActiveGroups[name] then activeshorad = true end if samgroup:GetProperty("SHORAD_ACTIVE") == true and activeshorad == false then activeshorad = true end if IsInZone and (not suppressed) and (not activeshorad) then --check any target in zone and not currently managed by SEAD - if samgroup:IsAlive() then + if samalive then -- switch on SAM local switch = false - if self.UseEmOnOff and switchedon < limit then + if navalparent and switchedon < limit then + -- per-unit naval: mark THIS unit RED; the group alarm arbiter + -- (_ApplyNavalAlarmState) applies the group switch at cycle end + switchedon = switchedon + 1 + switch = true + elseif self.UseEmOnOff and switchedon < limit then -- DONE: add emissions on/off samgroup:EnableEmission(true) switchedon = switchedon + 1 @@ -2448,13 +3017,13 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea self:__RedState(1,samgroup) end -- DONE Restrict on Distance - if shortsam == true and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then + if shortsam == true and (not navalparent) and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then self:T("Smoking") self:_SmokeUnits(samgroup) end -- link in to SHORAD if available -- DONE: Test integration fully - if self.ShoradLink and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early + if self.ShoradLink and (not navalparent) and (Distance < self.ShoradActDistance or Distance < blind ) then -- don't give SHORAD position away too early; ships never SHORAD-link local Shorad = self.Shorad --Functional.Shorad#SHORAD local radius = self.checkradius local ontime = self.ShoradTime @@ -2469,9 +3038,11 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end end --end alive else - if samgroup:IsAlive() and not suppressed and not activeshorad then + if samalive and not suppressed and not activeshorad then -- switch off SAM - if self.UseEmOnOff then + if navalparent then + -- per-unit naval: tracker-only; group alarm state applied by the arbiter + elseif self.UseEmOnOff then samgroup:EnableEmission(false) else samgroup:OptionAlarmStateGreen() @@ -2570,6 +3141,9 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea return statusreport end + -- apply naval group alarm states as the final word of this cycle + self:_ApplyNavalAlarmState() + if self.debug or self.verbose then local statusreport = GetReport() MESSAGE:New(statusreport:Text(),10):ToAll():ToLog() @@ -2605,9 +3179,22 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates for _,_data in pairs (samset) do local name = _data[1] - local samgroup = GROUP:FindByName(name) - if samgroup:IsAlive() then - if self.UseEmOnOff then + local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name + local samunit = nil + local samgroup = nil + if navalparent then + samunit = UNIT:FindByName(name) + samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent) + else + samgroup = GROUP:FindByName(name) + end + local samalive = false + if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false + elseif samgroup then samalive = samgroup:IsAlive() or false end + if samalive then + if navalparent then + self.SamStateTracker[name] = "RED" -- arbiter raises the group + elseif self.UseEmOnOff then -- DONE: add emissions on/off --samgroup:SetAIOn() samgroup:EnableEmission(true) @@ -3236,7 +3823,7 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end end for samName, samCoord in pairs(allSAMs) do - local params = self:_ResolveJammerParams(samName) + local params = (self._samJammerParams and self._samJammerParams[samName]) or self:_ResolveJammerParams(samName) if params then local survival = 1.0 for _, jammer in ipairs(self._jammerSnapshot) do @@ -3508,10 +4095,23 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea if self._jammerEnabled and self._jammedSAMs then for _, _data in pairs(samset) do local name = _data[1] + local navalparent = _data[8] -- per-unit naval entry: name is a UNIT name if self._jammedSAMs[name] and self.SamStateTracker[name] == "RED" then - local samgroup = GROUP:FindByName(name) - if samgroup and samgroup:IsAlive() then - if self.UseEmOnOff then + local samunit = nil + local samgroup = nil + if navalparent then + samunit = UNIT:FindByName(name) + samgroup = (samunit and samunit:GetGroup()) or GROUP:FindByName(navalparent) + else + samgroup = GROUP:FindByName(name) + end + local samalive = false + if navalparent then samalive = (samunit ~= nil) and samunit:IsAlive() or false + elseif samgroup then samalive = samgroup:IsAlive() or false end + if samalive then + if navalparent then + -- per-unit naval: tracker set GREEN below; arbiter darkens the group if no unit remains RED + elseif self.UseEmOnOff then samgroup:EnableEmission(false) else samgroup:OptionAlarmStateGreen() @@ -3545,6 +4145,214 @@ function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, wea end end + +-- =================================================================================================== +-- MANTIS Naval Extension: NAVYGROUP integration +-- Lets Ops.NavyGroup#NAVYGROUP objects (cruise control, missions, carrier ops) serve as +-- MANTIS-managed SAM shooters and/or EWR pickets: +-- mymantis:AddNavyGroup(navygroup, "SAM") -- managed shooter (or "EWR" = radiating picket) +-- navygroup:SetMANTIS(mymantis, "SAM") -- convenience, same effect +-- The ship group is added to the running set(s) regardless of name prefix; classification +-- and jamming resolve by hull type name as usual. MANTIS takes over emission/alarm +-- switching: the NAVYGROUP's default Emission/Alarmstate are neutralized so its +-- spawn-time option reset does not fight MANTIS. +-- =================================================================================================== +do + -- Guard MOOSE's ground relocation against ships. SEAD suppression (and any other + -- caller) invokes RelocateGroundRandomInRadius on suppressed SAM groups without a + -- category check; on a ship this clobbers the sailing route with a ground-formation + -- hop (and with onland=true can target a LAND point near a coast). Radar-off + -- evasion still applies to ships; only the ground move becomes a no-op. + if CONTROLLABLE and CONTROLLABLE.RelocateGroundRandomInRadius and not CONTROLLABLE._MANTISNavalRelocateGuard then + CONTROLLABLE._MANTISNavalRelocateGuard = true + local _origRelocate = CONTROLLABLE.RelocateGroundRandomInRadius + function CONTROLLABLE:RelocateGroundRandomInRadius( speed, radius, onroad, shortcut, formation, onland ) + if self.IsShip and self:IsShip() then + return self + end + return _origRelocate( self, speed, radius, onroad, shortcut, formation, onland ) + end + end + + --- [Internal] Check if any enemy surface combatant is within radius of a coordinate. + -- Builds a live enemy-ship SET_GROUP lazily on first use. + -- @param #MANTIS self + -- @param Core.Point#COORDINATE coord Ship position. + -- @param #number radius Trigger radius in meters. + -- @return #boolean found + function MANTIS:_EnemySurfaceInRange(coord, radius) + if self.NavalThreatSet == nil then + local own = string.lower(self.Coalition or "") + local enemy = (own == "red" and "blue") or (own == "blue" and "red") or nil + if not enemy then + self:E(self.lid.."NavalSurfaceWakeup: cannot derive enemy coalition from "..tostring(self.Coalition).." - disabling.") + self.NavalSurfaceWakeup = false + self.NavalThreatSet = false + return false + end + self.NavalThreatSet = SET_GROUP:New():FilterCoalitions(enemy):FilterCategoryShip():FilterStart() + end + if self.NavalThreatSet == false or not coord then return false end + local found = false + self.NavalThreatSet:ForEachGroupAlive( + function(grp) + if not found then + local gc = grp:GetCoordinate() + if gc and gc:Get2DDistance(coord) <= radius then found = true end + end + end) + return found + end + + --- Configure naval AUTONOMY outside EWR coverage. Default: ENABLED, 150km. + -- Ships mirror the ground-SAM umbrella concept: a ship group within CoverageKM + -- of any alive EWR group (including EWR-role ships or AWACS ships in the EWR + -- set) is managed normally -- alarm GREEN until MANTIS detects a threat inside + -- one of its units' envelopes. A group OUTSIDE coverage goes alarm RED and + -- searches/engages autonomously on its own radars until coverage is restored. + -- Full jamming suppression overrides autonomy and forces the group GREEN. + -- @param #MANTIS self + -- @param #boolean onoff Enable (true) or disable (false). + -- @param #number coveragekm (Optional) Coverage radius in KILOMETERS around each alive EWR (default 150). + -- @return #MANTIS self + function MANTIS:SetNavalAutonomy(onoff, coveragekm) + self.NavalAutonomy = onoff and true or false + if coveragekm then self.NavalAutonomyRadius = coveragekm * 1000 end + return self + end + + --- Enable/disable PER-UNIT classification and switching for naval groups. Default: ENABLED. + -- Mixed-hull ship groups (e.g. a carrier escorted by destroyers) get one SAM entry PER + -- SHIP -- each hull with its own type, range, state and jamming curve, switched via + -- UNIT:EnableEmission while the group stays alarm RED (weapons hot). Disable to revert + -- to legacy group-level naval handling (first matched hull defines the whole group). + -- @param #MANTIS self + -- @param #boolean onoff Enable (true) or disable (false). + -- @return #MANTIS self + function MANTIS:SetNavalPerUnit(onoff) + self.NavalPerUnit = onoff and true or false + return self + end + + --- Enable/disable the naval surface wake-up and optionally set a fixed trigger radius. + -- Default: ENABLED, radius = each ship's own engagement radius from SamDataNaval. + -- When enabled, a MANTIS-quiet ship goes active (RED / emissions on) whenever an + -- enemy surface combatant closes within the trigger radius, so anti-ship + -- engagements are not blocked by EMCON. Jamming still wins: a jammed ship stays + -- suppressed. Ships only -- ground SAM behavior is unchanged. + -- @param #MANTIS self + -- @param #boolean onoff Enable (true) or disable (false). + -- @param #number radiuskm (Optional) Fixed trigger radius in KILOMETERS for all ships. + -- @return #MANTIS self + function MANTIS:SetNavalSurfaceWakeup(onoff, radiuskm) + self.NavalSurfaceWakeup = onoff and true or false + self.NavalSurfaceWakeupRadius = radiuskm and radiuskm * 1000 or nil + return self + end + + --- Include or exclude naval SAMs from SEAD emission-shutdown evasion. Default: EXCLUDED. + -- Surface combatants engage inbound ARMs/ASMs with hard-kill defenses (SAMs, CIWS); + -- going emissions-dark would disable exactly those defenses. Ground SAM behavior is + -- unchanged. Takes effect at the next SAM table refresh (every 3rd detection cycle). + -- @param #MANTIS self + -- @param #boolean onoff If true, ships use SEAD EMCON evasion like ground SAMs. + -- @return #MANTIS self + function MANTIS:SetNavalSEAD(onoff) + self.SEADNaval = onoff and true or false + -- SEAD:UpdateSet() only ever ADDS names. When switching OFF mid-mission we + -- must actively remove ship groups already registered with the SEAD + -- instance, or they would keep evading until mission end. + if (not self.SEADNaval) and self.mysead and self.mysead.SEADGroupPrefixes then + for name,_ in pairs(self.mysead.SEADGroupPrefixes) do + local grp = GROUP:FindByName(name) + if grp and grp:IsShip() then + self.mysead.SEADGroupPrefixes[name] = nil + end + end + end + return self + end + + --- Add a NAVYGROUP to this MANTIS as SAM shooter and/or EWR picket. + -- IMPORTANT: MANTIS does not change ROE. NAVYGROUP defaults to Return Fire -- + -- set NavyGroup:SwitchROE(ENUMS.ROE.WeaponFree) in your mission if the ship + -- should engage detected targets rather than only return fire. + -- @param #MANTIS self + -- @param Ops.NavyGroup#NAVYGROUP NavyGroup The navy group to add. + -- @param #string Role "SAM" (default) = managed shooter, or "EWR" = unmanaged, always-radiating sensor/picket (coverage source). "BOTH" is deprecated and treated as SAM. + -- @return #MANTIS self + function MANTIS:AddNavyGroup(NavyGroup, Role) + local role = string.upper(Role or "SAM") + if role == "BOTH" then + -- Deprecated: a MANAGED shooter cannot double as a sensor -- its radars + -- are dark whenever MANTIS holds it GREEN, so EWR membership senses + -- nothing and only pollutes EWR-alive counting and the advanced-mode + -- network ratio. Use a SEPARATE ship group with Role="EWR" as a + -- radiating picket instead. + self:E(self.lid.."AddNavyGroup: Role BOTH is deprecated -- treating as SAM. Use a separate EWR-role group as a radiating picket.") + role = "SAM" + end + if not (NavyGroup and NavyGroup.ClassName and string.find(NavyGroup.ClassName,"NAVYGROUP",1,true)) then + self:E(self.lid.."AddNavyGroup: object passed is not a NAVYGROUP!") + return self + end + local grp = NavyGroup:GetGroup() + if not (grp and grp:IsAlive()) then + self:E(self.lid.."AddNavyGroup: NAVYGROUP has no alive GROUP (late-activated? Call after activation).") + return self + end + -- Hand emission/alarm control to MANTIS. NAVYGROUP re-applies its default + -- Alarmstate/Emission at (re)spawn, which would fight MANTIS switching: + -- set defaults to Emission ON / Alarmstate AUTO so the reset is neutral. + if NavyGroup.SetDefaultEmission then NavyGroup:SetDefaultEmission(true) end + if NavyGroup.SetDefaultAlarmstate then NavyGroup:SetDefaultAlarmstate(0) end -- 0 = Auto + -- NOTE: MANTIS deliberately does NOT touch ROE -- that is the mission + -- designer's call. Be aware OPSGROUP's default ROE is Return Fire: a + -- MANTIS-managed NAVYGROUP will radiate when RED but hold its weapons + -- unless the mission sets e.g. NavyGroup:SwitchROE(ENUMS.ROE.WeaponFree). + self._navyGroups = self._navyGroups or {} + self._navyGroups[grp:GetName()] = NavyGroup + if role == "SAM" then + self.SAM_Group:AddGroup(grp) + -- If MANTIS is already up, classify the new ship immediately; otherwise + -- SetSAMStartState will pick it up at Start. + if self.SAM_Table then + self:_RefreshSAMTable() + end + elseif role == "EWR" then + self.EWR_Group:AddGroup(grp) + if self.Adv_EWR_Group then + self.Adv_EWR_Group:AddGroup(grp) + end + else + self:E(self.lid.."AddNavyGroup: unknown Role "..tostring(Role).." - use SAM or EWR.") + end + self:I(self.lid..string.format("Added NAVYGROUP %s as %s",grp:GetName(),role)) + return self + end + + --- [Internal, static] Inject NAVYGROUP:SetMANTIS. Idempotent; safe if NAVYGROUP absent. + function MANTIS._InjectNavyGroupAPI() + if NAVYGROUP and not NAVYGROUP.SetMANTIS then + --- Register this NAVYGROUP with a MANTIS instance as SAM shooter and/or EWR picket. + -- @param Ops.NavyGroup#NAVYGROUP self + -- @param Functional.Mantis#MANTIS Mantis The MANTIS instance. + -- @param #string Role "SAM" (default) = managed shooter, or "EWR" = unmanaged, always-radiating sensor/picket (coverage source). "BOTH" is deprecated and treated as SAM. + -- @return Ops.NavyGroup#NAVYGROUP self + function NAVYGROUP:SetMANTIS(Mantis, Role) + if Mantis and Mantis.AddNavyGroup then + Mantis:AddNavyGroup(self, Role) + else + self:E(self.lid.."SetMANTIS: no valid MANTIS instance passed!") + end + return self + end + end + end + -- Try immediately in case NAVYGROUP is already loaded (also runs from MANTIS:New). + MANTIS._InjectNavyGroupAPI() +end + ----------------------------------------------------------------------- -- MANTIS end ------------------------------------------------------------------------ +----------------------------------------------------------------------- \ No newline at end of file From 55e048b322aee6e1b9fc7a14797405ce20ce613d Mon Sep 17 00:00:00 2001 From: leka1986 Date: Wed, 15 Jul 2026 18:39:46 +0200 Subject: [PATCH 11/11] Add group option change event support --- Moose Development/Moose/Core/Base.lua | 22 ++++++++++++++++++ Moose Development/Moose/Core/Event.lua | 32 ++++++++++++++++++++------ Moose Development/Moose/DCS.lua | 3 ++- 3 files changed, 49 insertions(+), 8 deletions(-) diff --git a/Moose Development/Moose/Core/Base.lua b/Moose Development/Moose/Core/Base.lua index 95d8858b7..6ec0a7d05 100644 --- a/Moose Development/Moose/Core/Base.lua +++ b/Moose Development/Moose/Core/Base.lua @@ -735,6 +735,12 @@ do -- Event Handling -- @function [parent=#BASE] OnEventPlayerEnterAircraft -- @param #BASE self -- @param Core.Event#EVENTDATA EventData The EventData structure. + + --- Occurs when an option is changed for an AI group. + -- The event identifies the initiating group, but does not provide the option ID or value. + -- @function [parent=#BASE] OnEventGroupChangeOption + -- @param #BASE self + -- @param Core.Event#EVENTDATA EventData The EventData structure. --- Occurs when a player creates a dynamic cargo object from the F8 ground crew menu. -- *** NOTE *** this is a workarounf for DCS not creating these events as of Aug 2024. @@ -880,6 +886,22 @@ end world.onEvent(Event) end + + --- Creation of a `S_EVENT_GROUP_CHANGE_OPTION` event. + -- @param #BASE self + -- @param DCS#Time EventTime The time stamp of the event. + -- @param DCS#Group Initiator The initiating group of the event. + function BASE:CreateEventGroupChangeOption( EventTime, Initiator ) + self:F( { EventTime, Initiator } ) + + local Event = { + id = EVENTS.GroupChangeOption, + time = EventTime, + initiator = Initiator, + } + + world.onEvent( Event ) + end --- Creation of a S_EVENT_NEW_DYNAMIC_CARGO event. -- @param #BASE self diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua index 267a46bdd..71270e026 100644 --- a/Moose Development/Moose/Core/Event.lua +++ b/Moose Development/Moose/Core/Event.lua @@ -278,6 +278,7 @@ EVENTS = { SimulationUnfreeze = world.event.S_EVENT_SIMULATION_UNFREEZE or -1, HumanAircraftRepairStart = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_START or -1, HumanAircraftRepairFinish = world.event.S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH or -1, + GroupChangeOption = world.event.S_EVENT_GROUP_CHANGE_OPTION or -1, -- dynamic cargo NewDynamicCargo = world.event.S_EVENT_NEW_DYNAMIC_CARGO or -1, DynamicCargoLoaded = world.event.S_EVENT_DYNAMIC_CARGO_LOADED or -1, @@ -291,23 +292,25 @@ EVENTS = { -- -- * A (Object.Category.)UNIT : A UNIT object type is involved in the Event. -- * A (Object.Category.)STATIC : A STATIC object type is involved in the Event. +-- * A GROUP : A DCS Group object is involved in the Event. -- -- @type EVENTDATA -- @field #number id The identifier of the event. -- --- @field DCS#Unit initiator (UNIT/STATIC/SCENERY) The initiating @{DCS#Unit} or @{DCS#StaticObject}. +-- @field DCS#Unit initiator (UNIT/STATIC/SCENERY/GROUP) The initiating @{DCS#Unit} or @{DCS#StaticObject}. For `S_EVENT_GROUP_CHANGE_OPTION`, this is a DCS Group. -- @field DCS#Object.Category IniObjectCategory (UNIT/STATIC/SCENERY) The initiator object category ( Object.Category.UNIT or Object.Category.STATIC ). -- @field DCS#Unit IniDCSUnit (UNIT/STATIC) The initiating @{DCS#Unit} or @{DCS#StaticObject}. -- @field #string IniDCSUnitName (UNIT/STATIC) The initiating Unit name. -- @field Wrapper.Unit#UNIT IniUnit (UNIT/STATIC) The initiating MOOSE wrapper @{Wrapper.Unit#UNIT} of the initiator Unit object. -- @field #string IniUnitName (UNIT/STATIC) The initiating UNIT name (same as IniDCSUnitName). --- @field DCS#Group IniDCSGroup (UNIT) The initiating {DCSGroup#Group}. --- @field #string IniDCSGroupName (UNIT) The initiating Group name. --- @field Wrapper.Group#GROUP IniGroup (UNIT) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object. --- @field #string IniGroupName UNIT) The initiating GROUP name (same as IniDCSGroupName). +-- @field DCS#Group IniDCSGroup (UNIT/GROUP) The initiating @{DCS#Group}. +-- @field #string IniDCSGroupName (UNIT/GROUP) The initiating Group name. +-- @field Wrapper.Group#GROUP IniGroup (UNIT/GROUP) The initiating MOOSE wrapper @{Wrapper.Group#GROUP} of the initiator Group object. +-- @field #string IniGroupName (UNIT/GROUP) The initiating GROUP name (same as IniDCSGroupName). +-- @field DCS#Group.Category IniGroupCategory (GROUP) The category of the initiating group. -- @field #string IniPlayerName (UNIT) The name of the initiating player in case the Unit is a client or player slot. -- @field #string IniPlayerUCID (UNIT) The UCID of the initiating player in case the Unit is a client or player slot and on a multi-player server. --- @field DCS#coalition.side IniCoalition (UNIT) The coalition of the initiator. +-- @field DCS#coalition.side IniCoalition (UNIT/GROUP) The coalition of the initiator. -- @field DCS#Unit.Category IniCategory (UNIT) The category of the initiator. -- @field #string IniTypeName (UNIT) The type name of the initiator. -- @@ -734,6 +737,12 @@ local _EVENTMETA = { Side = "I", Event = "OnEventHumanAircraftRepairFinish", Text = "S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH" + }, + [EVENTS.GroupChangeOption] = { + Order = 1, + Side = "I", + Event = "OnEventGroupChangeOption", + Text = "S_EVENT_GROUP_CHANGE_OPTION" }, -- dynamic cargo [EVENTS.NewDynamicCargo] = { @@ -1234,7 +1243,16 @@ function EVENT:onEvent( Event ) if Event.initiator then - Event.IniObjectCategory = Object.getCategory(Event.initiator) + if Event.id == EVENTS.GroupChangeOption then + Event.IniDCSGroup = Event.initiator + Event.IniDCSGroupName = Group.getName(Event.initiator) + Event.IniGroupName = Event.IniDCSGroupName + Event.IniGroup = GROUP:FindByName(Event.IniDCSGroupName) + Event.IniCoalition = Group.getCoalition(Event.initiator) + Event.IniGroupCategory = Group.getCategory(Event.initiator) + else + Event.IniObjectCategory = Object.getCategory(Event.initiator) + end if Event.IniObjectCategory == Object.Category.STATIC then --- diff --git a/Moose Development/Moose/DCS.lua b/Moose Development/Moose/DCS.lua index f726caf31..ddbfd0e8f 100644 --- a/Moose Development/Moose/DCS.lua +++ b/Moose Development/Moose/DCS.lua @@ -87,7 +87,8 @@ do -- world -- @field S_EVENT_SIMULATION_UNFREEZE = 58 -- @field S_EVENT_HUMAN_AIRCRAFT_REPAIR_START = 59 -- @field S_EVENT_HUMAN_AIRCRAFT_REPAIR_FINISH = 60 - -- @field S_EVENT_MAX = 61 + -- @field S_EVENT_GROUP_CHANGE_OPTION = 61 + -- @field S_EVENT_MAX = 62 --- The birthplace enumerator is used to define where an aircraft or helicopter has spawned in association with birth events. -- @type world.BirthPlace