Compare commits

...

130 Commits

Author SHA1 Message Date
Applevangelist a49288200e Merge remote-tracking branch 'origin/master' into develop 2025-12-29 14:01:43 +01:00
Applevangelist b94f36dfdf #AIRBASE - Added AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark)
#CSAR - Use `AIRBASE:GetMinimumBoundingCircleFromParkingSpots()` to determine if we're actually standing on an airbase somewhere
2025-12-29 14:01:10 +01:00
Applevangelist 8eae7ac26b #UTILS - Added functions to get a (minimum) bounding circle from a table of VEC2s 2025-12-29 13:43:45 +01:00
Applevangelist 7bac1d3b92 #UTILS - Added CALLSIGN.Intruder enumeration for the A-6E 2025-12-29 12:36:30 +01:00
Applevangelist b31b11cda8 #CSAR - Enforce pilot names on AI for saving
#SPAWN - Added use of CALLSIGN.Intruder for random callsigns of the A-6E
2025-12-29 12:35:59 +01:00
Applevangelist fc809964c7 Merge remote-tracking branch 'origin/master' into develop 2025-12-29 10:48:57 +01:00
Applevangelist d000ad76f8 #CSAR - Corrected Airbase dist finding solution 2025-12-29 10:48:23 +01:00
Applevangelist 3f5936bceb Merge remote-tracking branch 'origin/master' into develop 2025-12-28 17:56:48 +01:00
Applevangelist 29b244d843 #AIRWING - make airwing fill airbase storage with aircraft types when adding a Squadron and if necessary, ie not unlimited storage and storage lower than squadron assets number 2025-12-28 17:53:49 +01:00
Applevangelist fef1976a6f Merge remote-tracking branch 'origin/master' into develop 2025-12-28 16:52:14 +01:00
Applevangelist 1eb6dbc7f2 xx 2025-12-28 16:51:41 +01:00
Applevangelist d16eb74e1b Merge remote-tracking branch 'origin/master' into develop 2025-12-28 14:06:38 +01:00
Applevangelist 945b9bdf31 #ZONE - Typo in docu 2025-12-28 14:05:24 +01:00
Applevangelist b75e383956 #SCORING - enable saving and loading of per-game name scores for persistence across restarts 2025-12-28 14:05:24 +01:00
Applevangelist 15de1d58b0 #PLAYERTASK
* Changed startup timing slightly to allow task reloading working better
* Changed coordinate output format for INTERCEPT tasks on non A2A managers to A2A
2025-12-28 14:05:24 +01:00
Applevangelist 79ce3871ea #INTEL - Added option to read properties CorridorCeiling and CorridorFloor from Zone properties to allow individual settings per zone 2025-12-28 14:05:24 +01:00
Thomas 646fa8b7e6 Merge pull request #2485 from FlightControl-Master/master
Merge
2025-12-28 09:01:42 +01:00
Thomas 84c68ccf14 Merge pull request #2484 from shaji-Dev/master
[ADDED] Hidden options to SPAWNSTATIC
2025-12-28 09:00:26 +01:00
Shafik 93d0a8b674 [ADDED] Hidden options to SPAWNSTATIC 2025-12-28 09:43:45 +02:00
Shafik 8a33ccd356 [ADDED] Hidden options to SPAWNSTATIC 2025-12-28 02:49:14 +02:00
Thomas 7ff8978ca7 Merge pull request #2483 from FlightControl-Master/master
Merge from master
2025-12-24 08:21:06 +01:00
Thomas 9473cc2b27 Merge pull request #2482 from leka1986/patch-12
Bug fixes and functions.
2025-12-24 08:20:03 +01:00
leka1986 448d668b44 Bug fixes and functions.
Fixed a bug where units could still get items all though not in load zone.

Added CanGetCrates, it's a function where user can hook custom conditions.

Added CanGetUnits, same as the above.

Added CanBuildCrates, same as the first one.
2025-12-24 00:00:30 +01:00
Applevangelist 635c8dc2ad Merge remote-tracking branch 'origin/master' into develop 2025-12-23 17:17:34 +01:00
Applevangelist 8c0e021e82 #CTLD - Make FSM event CratesDropped useable for tasking 2025-12-23 17:16:57 +01:00
Applevangelist 022854fa37 Merge remote-tracking branch 'origin/master' into develop 2025-12-22 14:10:14 +01:00
Applevangelist a72d3afd1c xx 2025-12-22 14:09:37 +01:00
Applevangelist 1f062fe7b4 Merge remote-tracking branch 'origin/master' into develop 2025-12-22 12:22:13 +01:00
Applevangelist 68aab10902 xx 2025-12-22 12:21:42 +01:00
Applevangelist b9be0341f5 Merge remote-tracking branch 'origin/master' into develop 2025-12-21 19:17:31 +01:00
Applevangelist b712c44567 xx 2025-12-21 19:17:02 +01:00
Applevangelist 12a118d08f xx 2025-12-21 18:48:33 +01:00
Applevangelist b1b9db7333 xx 2025-12-21 18:48:24 +01:00
Applevangelist 28e08035f2 Merge remote-tracking branch 'origin/master' into develop
# Conflicts:
#	Moose Development/Moose/Ops/EasyA2G.lua
#	Moose Development/Moose/Ops/EasyGCICAP.lua
2025-12-21 15:40:46 +01:00
Applevangelist 5f920a46fd #PLAYERTASK - Added INTEL Corridors; added persistence for certain tasks as option 2025-12-21 15:39:19 +01:00
Applevangelist 23e3aefaf1 Files 2025-12-21 15:38:47 +01:00
Applevangelist 74e38f9755 #MANTIS - Added INTEL Corridors 2025-12-21 15:38:39 +01:00
Applevangelist 69a781a710 #INTEL - Added Corridors 2025-12-21 15:38:30 +01:00
Applevangelist 2c82513e54 #EASYGCICAP - Added INTEL Corridors 2025-12-21 15:38:14 +01:00
Applevangelist 8eeff7efa2 #EASYAG Added 2025-12-21 15:37:46 +01:00
Applevangelist 7fcac6159c #AWACS - Added INTEL Corridors 2025-12-21 15:37:34 +01:00
Applevangelist 3cac18f634 Merge remote-tracking branch 'origin/master' into develop 2025-12-21 12:50:04 +01:00
Applevangelist a5eef3087b #CSAR - added option for Smoke Closest MASH, adjusted logic for airbase rescues 2025-12-21 12:49:43 +01:00
Applevangelist 1fbfc8f039 Merge remote-tracking branch 'origin/master' into develop 2025-12-21 11:44:00 +01:00
Applevangelist af891f3fc2 #2479 Added S-300VM/S-300V4/S-400 SAM data 2025-12-21 11:43:29 +01:00
Thomas 47bb760d86 Merge pull request #2481 from FlightControl-Master/master
Merge
2025-12-21 09:26:19 +01:00
Thomas 8e47a17c96 Merge pull request #2480 from shaji-Dev/master
[ADDED] Altitude AGL option for POSITIONABLE
2025-12-21 09:25:35 +01:00
Shafik c2c78dfe78 [ADDED] Altitude AGL option for POSITIONABLE 2025-12-21 09:15:44 +02:00
Thomas c24acd2681 Merge pull request #2478 from FlightControl-Master/master
Merge
2025-12-20 14:06:52 +01:00
Thomas 26742432f5 Merge pull request #2477 from leka1986/patch-11
Update CTLD.lua
2025-12-20 14:06:06 +01:00
leka1986 6a13e593ec Update CTLD.lua
Added:
OnAfrerGetCrates, Only triggers when player uses "Get"

Added:
OnAfterRemoveCratedNearBy, Only triggers when player actively uses Remove crates near by or remove units near by.

Added: 
Partial load. currently, if a player land 3 crates (Farp) and want to partially load another farp, even though it can not fit, some want to load partially, this allows for it. 
Player will be able to see what actually fits in the menu, but if they want, they can open the menu again, select the item, then it will say "Get" "Crates limit reached" and "Partially load"

Added:
AddStaticsCargo can now accept a last parms, UnitType, this parm will make the cargo only visible in the menu to that passed unitType.

Removed Get and Load from CH-47 for the statics if enableChinookGCLoading is enabled. This will make it less confusing, as it didn't allow players do load anything except Ground crew for statics.
2025-12-19 18:44:13 +01:00
Thomas b63fe40cc8 Merge pull request #2475 from FlightControl-Master/master
Merge from master
2025-12-18 10:20:06 +01:00
Thomas 8d8b886ad6 Merge pull request #2474 from shaji-Dev/master
Removed MARKEROPS_BASE:_MatchTag log spam
2025-12-18 10:19:13 +01:00
Shafik 2e6cc61450 Removed MARKEROPS_BASE:_MatchTag log spam 2025-12-18 10:31:45 +02:00
Shafik 6859dc9142 Merge branch 'master' of https://github.com/shaji-Dev/MOOSE
# Conflicts:
#	Moose Development/Moose/Ops/CTLD.lua
2025-12-18 10:03:54 +02:00
Shafik b871f728df PULL 2025-12-18 10:03:45 +02:00
Thomas e41eec2238 Merge pull request #2473 from FlightControl-Master/master
Update Database.lua
2025-12-17 19:37:36 +01:00
Thomas 87b87fa2ca Update Database.lua 2025-12-17 19:34:06 +01:00
Applevangelist 3c996b6dd7 Merge remote-tracking branch 'origin/master' into develop 2025-12-17 11:32:28 +01:00
Applevangelist a39f39646d #MARKEROPS - Put case sensitivity switching into own functions 2025-12-17 11:31:55 +01:00
Thomas 2012e81acb Merge pull request #2471 from FlightControl-Master/master
Merge from master
2025-12-16 08:40:00 +01:00
Thomas 157f9a82b1 Merge pull request #2470 from leka1986/patch-10
Update CTLD.lua
2025-12-16 08:39:02 +01:00
leka1986 795f58dc9b Update CTLD.lua
Fixing error when the savefile loads statics in the onafterLoad, it blows up.

attempt to call method 'GetStaticResourceMap' (a nil value)
2025-12-15 22:42:29 +01:00
Thomas 4c841fd1d2 Merge pull request #2469 from FlightControl-Master/master
Merge from master
2025-12-15 17:41:37 +01:00
Thomas 976f92e5b1 Merge pull request #2468 from leka1986/patch-9
Update CTLD.lua
2025-12-15 17:41:06 +01:00
leka1986 25daa67554 Update CTLD.lua
Fixed error when using Dynamic cargo 
"concatenate local 'gname'"

and the AddStaticCargo did not respect the template staticType.
2025-12-15 17:29:54 +01:00
Applevangelist ef5c7e5972 Merge remote-tracking branch 'origin/master' into develop 2025-12-15 12:13:32 +01:00
Applevangelist 33ff723bd8 Further refinement for acoustic detection and smoke 2025-12-15 12:13:04 +01:00
Applevangelist 21c744b27a Merge remote-tracking branch 'origin/master' into develop 2025-12-15 09:41:13 +01:00
Applevangelist 34f3e24d3a #MANTIS - Fixing over-smoke and ensure helos are not attacked by long-range systems 2025-12-15 09:40:42 +01:00
Applevangelist 38b36c0344 Merge remote-tracking branch 'origin/master' into develop 2025-12-14 16:05:44 +01:00
Applevangelist b6a90d4633 #GROUP Added GROUP:SetFormation(Formation) 2025-12-14 16:05:18 +01:00
Applevangelist 5145dd14ea Merge remote-tracking branch 'origin/master' into develop 2025-12-14 14:37:36 +01:00
Applevangelist 0e960b77b9 #MANTIS - Mantis can use acoustic detection, e.g. for Helos sneaking in on our position: my_mantis:SetAccousticDetectionOn(2000,{Unit.Category.HELICOPTER}) 2025-12-14 14:37:00 +01:00
Applevangelist 093526f4ff #ZONE - Added optional coalition filter on GetScannedUnitSet() and GetScannedGroupSet() 2025-12-14 14:35:50 +01:00
Applevangelist 8160aad1bf #INTELLIGENCE - We can optionally detect units accousticly 2025-12-14 14:34:54 +01:00
Applevangelist 51f40081f3 docu header 2025-12-12 12:51:44 +01:00
Applevangelist 7e878b5702 Merge remote-tracking branch 'origin/develop' into develop 2025-12-12 10:51:58 +01:00
Applevangelist 804e0ed69d Docu fixes 2025-12-12 10:51:55 +01:00
Thomas 46c0d47175 Update build-docs.yml 2025-12-11 16:37:30 +01:00
Applevangelist 147df1498e Merge remote-tracking branch 'origin/master' into develop 2025-12-11 15:48:41 +01:00
Applevangelist f9410a9169 #EASYA2G - Initial release 2025-12-11 15:48:13 +01:00
Applevangelist a88750908e #WAREHOUSE - Nil fix 2025-12-11 15:43:33 +01:00
Thomas aca3080c9a Merge pull request #2467 from leka1986/patch-8
Update CTLD.lua
2025-12-11 15:10:01 +01:00
leka1986 948158e85a Update CTLD.lua
Fixed bugs, 

Added RemoveStockUnits.

Tested and tested, works as intended.
2025-12-11 11:27:05 +01:00
Applevangelist 9b3aa423aa xx 2025-12-11 09:34:19 +01:00
Applevangelist 07876cb46c Merge remote-tracking branch 'origin/develop' into develop 2025-12-10 11:30:59 +01:00
Applevangelist de48701d15 Merge remote-tracking branch 'origin/master' into develop 2025-12-10 11:30:55 +01:00
Applevangelist c5678404d5 Merge remote-tracking branch 'origin/master' into develop
# Conflicts:
#	Moose Development/Moose/Functional/PseudoATC.lua
2025-12-10 11:29:39 +01:00
Thomas 4e2dcb1c05 Merge pull request #2466 from FlightControl-Master/master
xx
2025-12-10 09:58:38 +01:00
Thomas 98539eee87 Merge branch 'develop' into master 2025-12-10 09:58:13 +01:00
Thomas 223c1650d9 Update PseudoATC.lua 2025-12-10 09:48:16 +01:00
Thomas aa2133e72a Update build-includes.yml 2025-12-10 09:43:20 +01:00
Applevangelist 498d58599b xx 2025-12-10 09:38:49 +01:00
Thomas a0c0fe36ba Merge pull request #2465 from FlightControl-Master/master
Merge to master
2025-12-09 20:09:09 +01:00
Thomas 76b977ff98 Merge pull request #2464 from leka1986/patch-7
Added Get Units and New stuff for the C-130J
2025-12-09 20:07:07 +01:00
leka1986 878eaf4301 Added Get Units and New stuff for the C-130J
Added

* UseC130LoadAndUnload toggle, false by default.
* C130basetype = "cds_crate" -- only for the C-130
* Extended AddCratesCargo & AddCratesCargoNoMove with another parms where we can force Typename for the static for the C-130, even though a parms passed before it with another shape. the last parms will still be forced only if the unit is C-130J. else, the last parms will not be used.

Example usage:
Foothold_ctld:AddCratesCargo("FARP",{"CTLD_TROOP_FOB"},CTLD_CARGO.Enum.FOB,3,1500,10, "FARP",nil,nil,nil,"Cargos","ammo_cargo",nil, "cds_crate")
Blocked the build of crates if still inside C-130J and CH-47F using 15 Meters for the C-130 and 5 meters for the CH-47.

ListCargo will now display what's inside besed on _GetUnitCapabilities / 2


** Manage Units, New menu only for the C-130J.

* New menu under CTLD. Get Units, Remove units nearby.
* Addunits function.
function CTLD:AddUnits(Name,Templates,Type,Stock,SubCategory,UnitTypes)
Example usage.

Foothold_ctld:AddUnits("Humvee",{"CTLD_CARGO_HMMWV"},CTLD_CARGO.Enum.VEHICLE,10, "ANTI TANK")

Those will be placed on a safe distance using same method as crates based on length and (idx-1)*2.5 + length.

* Added FSM, OnAfterUnitsSpawn. No internal, only Hook.

* Added 3 functions,
AddStockUnits
SetStockUnits
GetStockUnits

Added documents.
2025-12-09 19:45:56 +01:00
Applevangelist 240d3460d7 xx 2025-12-07 15:13:50 +01:00
Applevangelist ae0d405350 #PSEUDOATC - Added functionality to read the Radio.lua from the map data and display radio information per Airport 2025-12-07 15:12:37 +01:00
Applevangelist 45fc0eb9da Merge remote-tracking branch 'origin/master' into develop 2025-12-07 13:39:56 +01:00
Applevangelist 3327c3b24d #GROUP Small fix for getting templates data from dynamic spawns (there are no template data!) 2025-12-07 13:39:23 +01:00
Applevangelist c6521d9c38 Merge remote-tracking branch 'origin/master' into develop 2025-12-07 13:02:45 +01:00
Applevangelist 049529ecf0 #CTLD Allow hover extracting Troop objects 2025-12-07 13:02:17 +01:00
Applevangelist c2b8eea010 #RAT small fix for Tonfinal being nil sometimes 2025-12-07 13:01:53 +01:00
Thomas da8856c907 Merge pull request #2463 from FlightControl-Master/master
Merge from master
2025-12-06 11:50:12 +01:00
Thomas 04f7bb7cc7 Merge pull request #2458 from leka1986/patch-3
Convert cargo type to string for logging
2025-12-06 11:43:19 +01:00
Thomas 75c01fbe86 Merge pull request #2459 from leka1986/patch-4
Add check for IniGroup in HandleEventShot
2025-12-06 11:43:09 +01:00
Thomas 03b79351db Merge pull request #2460 from leka1986/patch-5
Update Scenery.lua
2025-12-06 11:42:51 +01:00
Thomas 33429766a0 Merge pull request #2461 from leka1986/patch-6
Update Set.lua
2025-12-06 11:42:39 +01:00
leka1986 83464090d6 Update Set.lua
removed duplicate Filter = {},
2025-12-06 10:20:03 +01:00
leka1986 d77b2655b1 Update Scenery.lua
attempt to call method getPoint (a nil value)
2025-12-06 10:16:15 +01:00
leka1986 31a537748e Add check for IniGroup in HandleEventShot
Added a check for IniGroup in HandleEventShot to prevent processing if it's not present.
Error in 2 instances, attempt to index field IniGroup ( a nil value )
2025-12-06 10:13:37 +01:00
leka1986 e873b097cf Convert cargo type to string for logging
Concatenate local 'type' Nil value
2025-12-06 10:10:18 +01:00
Frank 07a3095392 Merge branch 'master' into develop 2025-12-02 10:36:55 +01:00
Frank e1b5c7f9c2 Merge pull request #2457 from shaji-Dev/master
[ADDED] C-130J loadouts enums and other missing weapons
2025-12-02 10:36:01 +01:00
Shafik f005cdbc71 [ADDED] C-130J loadouts enums and other missing weapons. 2025-12-02 11:27:57 +02:00
Applevangelist 7fa360f32c Merge remote-tracking branch 'origin/master' into develop 2025-11-28 12:49:54 +01:00
Applevangelist 30c6937910 #CTLD Fix find crates also looking into static cargos 2025-11-28 12:48:26 +01:00
Applevangelist 28724f396c Merge remote-tracking branch 'origin/master' into develop 2025-11-28 10:20:13 +01:00
Applevangelist fb3ca6d9d5 #SCORING nil check added 2025-11-28 10:19:46 +01:00
Applevangelist 6e75e58952 #RESCUEHELO small fix 2025-11-28 09:16:08 +01:00
Thomas 0abb0db2a3 Merge pull request #2456 from FlightControl-Master/master
Merge from master
2025-11-27 18:07:18 +01:00
Thomas edb42bbbe9 Merge pull request #2455 from shaji-Dev/master
[ADDED] C-130J Utils
2025-11-27 18:06:41 +01:00
Shafik 3bebe7f69c [ADDED] C-130J Utils 2025-11-27 18:25:19 +02:00
Applevangelist 7605b582d1 Merge branch 'master' into develop 2025-11-27 09:22:11 +01:00
Applevangelist 6ed031762b #BUGFIXING 2025-11-27 09:21:17 +01:00
Thomas 945efc0516 Merge pull request #2454 from FlightControl-Master/master
Update Artillery.lua
2025-11-26 10:11:11 +01:00
Thomas 4e342a59be Update Artillery.lua
#ARTY fix a small error for coordinate of smoke shell creation
2025-11-26 10:10:10 +01:00
Thomas 8e517bb9cf Merge pull request #2453 from FlightControl-Master/master
Update Net.lua
2025-11-24 09:35:23 +01:00
Thomas dacf5125bf Update Net.lua
#NET Fix for GetPlayerIDByName()
2025-11-24 09:24:37 +01:00
40 changed files with 3856 additions and 476 deletions
+2 -2
View File
@@ -77,11 +77,11 @@ jobs:
- name: Install markdown (prereq for LuaDocumentor)
run: |
sudo luarocks install markdown 0.32-2
sudo luarocks install markdown 0.33-1
- name: Install penlight (prereq for LuaDocumentor)
run: |
sudo luarocks install penlight 1.11.0-1
sudo luarocks install penlight 1.14.0-3
- name: Install metalua-compiler (prereq for LuaDocumentor)
run: |
+1 -1
View File
@@ -105,7 +105,7 @@ jobs:
if: ${{ vars.SKIP_LUACHECK != true }}
continue-on-error: true
run: |
luacheck --std=lua51c --config=.luacheckrc -gurasqq "Moose Development/Moose"
luacheck --std=lua51c --config=.luacheckrc -guras "Moose Development/Moose"
- name: Run LuaSrcDiet
run: |
+2 -2
View File
@@ -11,8 +11,8 @@
-- @module AI.AI_A2G_BAI
-- @image AI_Air_To_Ground_Engage.JPG
-- @type AI_A2G_BAI
-- @extends AI.AI_A2A_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends.
--- @type AI_A2G_BAI
-- @extends AI.AI_Air_Engage#AI_A2A_Engage -- TODO: Documentation. This class does not exist, unable to determine what it extends.
--- Implements the core functions to intercept intruders. Use the Engage trigger to intercept intruders.
--
+1 -1
View File
@@ -31,7 +31,7 @@
-- @module AI.AI_CAP
-- @image AI_Combat_Air_Patrol.JPG
-- @type AI_CAP_ZONE
--- @type AI_CAP_ZONE
-- @field Wrapper.Controllable#CONTROLLABLE AIControllable The @{Wrapper.Controllable} patrolling.
-- @field Core.Zone#ZONE_BASE TargetZone The @{Core.Zone} where the patrol needs to be executed.
-- @extends AI.AI_Patrol#AI_PATROL_ZONE
+1 -1
View File
@@ -1820,7 +1820,7 @@ function DATABASE:_EventOnPlayerEnterUnit( Event )
if Event.IniDCSUnit then
-- Player entering a CA slot
if Event.IniObjectCategory == 1 and Event.IniGroup and Event.IniGroup:IsGround() then
if Event.IniObjectCategory == 1 and Event.IniUnit and Event.IniUnit:IsGround() then
local IsPlayer = Event.IniDCSUnit:getPlayerName()
if IsPlayer then
+26 -13
View File
@@ -50,7 +50,7 @@ MARKEROPS_BASE = {
ClassName = "MARKEROPS",
Tag = "mytag",
Keywords = {},
version = "0.1.4",
version = "0.1.5",
debug = false,
Casesensitive = true,
}
@@ -59,9 +59,8 @@ MARKEROPS_BASE = {
-- @param #MARKEROPS_BASE self
-- @param #string Tagname Name to identify us from the event text.
-- @param #table Keywords Table of keywords recognized from the event text.
-- @param #boolean Casesensitive (Optional) Switch case sensitive identification of Tagname. Defaults to true.
-- @return #MARKEROPS_BASE self
function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive)
function MARKEROPS_BASE:New(Tagname,Keywords)
-- Inherit FSM
local self=BASE:Inherit(self, FSM:New()) -- #MARKEROPS_BASE
@@ -72,11 +71,7 @@ function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive)
self.Keywords = Keywords or {} -- #table - might want to use lua regex here, too
self.debug = false
self.Casesensitive = true
if Casesensitive and Casesensitive == false then
self.Casesensitive = false
end
-----------------------
--- FSM Transitions ---
-----------------------
@@ -145,7 +140,7 @@ function MARKEROPS_BASE:New(Tagname,Keywords,Casesensitive)
end
--- (internal) Handle events.
--- (Internal) Handle events.
-- @param #MARKEROPS_BASE self
-- @param Core.Event#EVENTDATA Event
function MARKEROPS_BASE:OnEventMark(Event)
@@ -201,15 +196,17 @@ function MARKEROPS_BASE:OnEventMark(Event)
end
end
--- (internal) Match tag.
--- (Internal) Match tag.
-- @param #MARKEROPS_BASE self
-- @param #string Eventtext Text added to the marker.
-- @return #boolean
function MARKEROPS_BASE:_MatchTag(Eventtext)
local matches = false
if not self.Casesensitive then
--self:I(self.lid .. "Casesensitive "..tostring(self.Casesensitive))
if self.Casesensitive == false then
--self:I(self.lid .. "Marker non-casesensitive "..Eventtext)
local type = string.lower(self.Tag) -- #string
if string.find(string.lower(Eventtext),type) then
if string.find(string.lower(Eventtext),type,1,true) then
matches = true --event text contains tag
end
else
@@ -221,7 +218,7 @@ function MARKEROPS_BASE:_MatchTag(Eventtext)
return matches
end
--- (internal) Match keywords table.
--- (Internal) Match keywords table.
-- @param #MARKEROPS_BASE self
-- @param #string Eventtext Text added to the marker.
-- @return #table
@@ -286,6 +283,22 @@ function MARKEROPS_BASE:onenterStopped(From,Event,To)
self:UnHandleEvent(EVENTS.MarkRemoved)
end
--- Switch off case sensitive matching
-- @param #MARKEROPS_BASE self
-- @return self
function MARKEROPS_BASE:SwitchCaseSensitiveOff()
self.Casesensitive = false
return self
end
--- Switch on case sensitive matching
-- @param #MARKEROPS_BASE self
-- @return self
function MARKEROPS_BASE:SwitchCaseSensitiveOn()
self.Casesensitive = true
return self
end
--------------------------------------------------------------------------
-- MARKEROPS_BASE Class Definition End.
--------------------------------------------------------------------------
-1
View File
@@ -89,7 +89,6 @@ do -- SET_BASE
Index = {},
Database = nil,
CallScheduler = nil,
Filter = {},
FilterCoalitionNumbers = {
[coalition.side.RED+1] = "red",
[coalition.side.BLUE+1] = "blue",
+8
View File
@@ -1667,6 +1667,9 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth )
-- If RandomizeUnits, then Randomize the formation at the start point.
if self.SpawnRandomizeUnits then
if self.SpawnRandomizePosition then
PointVec3 = COORDINATE:New( SpawnTemplate.x, SpawnTemplate.route.points[1].alt, SpawnTemplate.y )
end
for UnitID = 1, #SpawnTemplate.units do
local RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius )
if (SpawnZone) then
@@ -3518,6 +3521,11 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2
max = 18
ctable = CALLSIGN.F15E
end
if SpawnTemplate.units[1].type == "A6E" then
min = 4
max = 18
ctable = CALLSIGN.Intruder
end
local callsignnr = math.random(min,max)
local callsignname = "Enfield"
for name, value in pairs(ctable) do
@@ -334,6 +334,31 @@ function SPAWNSTATIC:InitCargo(IsCargo)
return self
end
--- Hide the static on the map view (visible to game master slots!).
-- @param #SPAWN self
-- @param #boolean OnOff Defaults to true
-- @return #SPAWN The SPAWN object
function SPAWNSTATIC:InitHiddenOnMap(OnOff)
self.SpawnHiddenOnMap = OnOff == false and false or true
return self
end
--- Hide the static on MFDs (visible to game master slots!).
-- @param #SPAWN self
-- @return #SPAWN The SPAWN object
function SPAWNSTATIC:InitHiddenOnMFD()
self.SpawnHiddenOnMFD = true
return self
end
--- Hide the static on planner (visible to game master slots!).
-- @param #SPAWN self
-- @return #SPAWN The SPAWN object
function SPAWNSTATIC:InitHiddenOnPlanner()
self.SpawnHiddenOnPlanner = true
return self
end
--- Initialize as dead.
-- @param #SPAWNSTATIC self
-- @param #boolean IsDead If true, this static is dead.
@@ -533,6 +558,19 @@ function SPAWNSTATIC:_SpawnStatic(Template, CountryID)
Template.mass=self.InitStaticCargoMass
end
-- hiding options
if self.SpawnHiddenOnPlanner then
Template.hiddenOnPlanner=true
end
if self.SpawnHiddenOnMFD then
Template.hiddenOnMFD=true
end
if self.SpawnHiddenOnMap then
Template.hidden=self.SpawnHiddenOnMap
end
if self.InitLinkUnit then
Template.linkUnit=self.InitLinkUnit:GetID()
Template.linkOffset=true
+30 -14
View File
@@ -783,7 +783,7 @@ end
-- @param #ZONE_BASE self
-- @param #string From
-- @param #string Event
-- @param #string to
-- @param #string To
-- @return #ZONE_BASE self
function ZONE_BASE:onafterTriggerRunCheck(From,Event,To)
if self:GetState() ~= "TriggerStopped" then
@@ -1158,7 +1158,7 @@ end
-- myzone:Scan({Object.Category.UNIT},{Unit.Category.GROUND_UNIT})
-- local IsAttacked = myzone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )
self.ScanData = {}
self.ScanData.Coalitions = {}
self.ScanData.Scenery = {}
@@ -1168,7 +1168,7 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )
local ZoneCoord = self:GetCoordinate():SetAlt()
local ZoneRadius = self:GetRadius()
--self:F({ZoneCoord = ZoneCoord, ZoneRadius = ZoneRadius, ZoneCoordLL = ZoneCoord:ToStringLLDMS()})
--self:I({x = ZoneCoord.x, y=ZoneCoord.y, z=ZoneCoord.z, ZoneRadius = ZoneRadius})
local SphereSearch = {
id = world.VolumeType.SPHERE,
@@ -1180,6 +1180,7 @@ function ZONE_RADIUS:Scan( ObjectCategories, UnitCategories )
local function EvaluateZone( ZoneObject )
--if ZoneObject:isExist() then --FF: isExist always returns false for SCENERY objects since DCS 2.2 and still in DCS 2.5
if ZoneObject and self:IsVec3InZone(ZoneObject:getPoint()) then
-- Get object category.
@@ -1265,38 +1266,48 @@ end
--- Get a set of scanned units.
-- @param #ZONE_RADIUS self
-- @param #number Coalition (optional) Filter for this coalition only.
-- @return Core.Set#SET_UNIT Set of units and statics inside the zone.
function ZONE_RADIUS:GetScannedSetUnit()
local SetUnit = SET_UNIT:New()
function ZONE_RADIUS:GetScannedSetUnit(Coalition)
self.SetUnit = self.SetUnit or SET_UNIT:New()
self.SetUnit:Clear(false)
self.SetUnit.Set={}
if self.ScanData then
for ObjectID, UnitObject in pairs( self.ScanData.Units ) do
local UnitObject = UnitObject -- DCS#Unit
if UnitObject:isExist() then
local FoundUnit = UNIT:FindByName( UnitObject:getName() )
if FoundUnit then
SetUnit:AddUnit( FoundUnit )
local FoundCoalition = FoundUnit and FoundUnit:GetCoalition() or nil
local includeoncoalition = true
if Coalition ~= nil and FoundCoalition==Coalition then includeoncoalition = true else includeoncoalition = false end
if Coalition == nil then includeoncoalition = true end
--self:I(string.format("Unit name %s coalition %s filter coalition = %s include = %s",FoundUnit:GetName(),tostring(FoundCoalition),tostring(Coalition),tostring(includeoncoalition)))
if FoundUnit and includeoncoalition then
self.SetUnit:AddUnit( FoundUnit )
else
local FoundStatic = STATIC:FindByName( UnitObject:getName(), false )
if FoundStatic then
SetUnit:AddUnit( FoundStatic )
self.SetUnit:AddUnit( FoundStatic )
end
end
end
end
end
return SetUnit
return self.SetUnit
end
--- Get a set of scanned groups.
-- @param #ZONE_RADIUS self
-- @param #number Coalition (optional) Filter for this coalition only.
-- @return Core.Set#SET_GROUP Set of groups.
function ZONE_RADIUS:GetScannedSetGroup()
function ZONE_RADIUS:GetScannedSetGroup(Coalition)
self.ScanSetGroup=self.ScanSetGroup or SET_GROUP:New() --Core.Set#SET_GROUP
self.ScanSetGroup:Clear(false)
self.ScanSetGroup.Set={}
if self.ScanData then
@@ -1305,7 +1316,12 @@ function ZONE_RADIUS:GetScannedSetGroup()
if UnitObject:isExist() then
local FoundUnit=UNIT:FindByName(UnitObject:getName())
if FoundUnit then
local FoundCoalition = FoundUnit and FoundUnit:GetCoalition() or nil
local includeoncoalition = true
if Coalition ~= nil and FoundCoalition==Coalition then includeoncoalition = true else includeoncoalition = false end
if Coalition == nil then includeoncoalition = true end
--self:I(string.format("Unit name %s coalition %s filter coalition = %s include = %s",FoundUnit:GetName(),tostring(FoundCoalition),tostring(Coalition),tostring(includeoncoalition)))
if FoundUnit and includeoncoalition then
local group=FoundUnit:GetGroup()
self.ScanSetGroup:AddGroup(group)
end
@@ -3380,7 +3396,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories )
self.ScanData.Scenery[SceneryType] = self.ScanData.Scenery[SceneryType] or {}
self.ScanData.Scenery[SceneryType][SceneryName] = SCENERY:Register( SceneryName, ZoneObject )
table.insert(self.ScanData.SceneryTable,self.ScanData.Scenery[SceneryType][SceneryName])
--self:T( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } )
--self:I( { SCENERY = self.ScanData.Scenery[SceneryType][SceneryName] } )
end
end
@@ -27,7 +27,7 @@
--
-- ### Author: **funkyfranky**
--
-- ### Contributions: FlightControl
-- ### Contributions: FlightControl, Applevangelist
--
-- ====
-- @module Functional.Artillery
@@ -780,7 +780,7 @@ ARTY.db={
--- Arty script version.
-- @field #string version
ARTY.version="1.3.3"
ARTY.version="1.3.4"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2268,7 +2268,7 @@ function ARTY._FuncTrack(weapon, self, target)
if _dist<target.radius then
-- Get random coordinate within a certain radius.
local _cr=_coord:GetRandomCoordinateInRadius(_data.target.radius)
local _cr=_coord:GetRandomCoordinateInRadius(target.radius)
-- Fire smoke at this coordinate.
_cr:Smoke(self.smokeColor)
+346 -39
View File
@@ -22,7 +22,7 @@
-- @module Functional.Mantis
-- @image Functional.Mantis.jpg
--
-- Last Update: August 2025
-- Last Update: December 2025
-------------------------------------------------------------------------
--- **MANTIS** class, extends Core.Base#BASE
@@ -61,10 +61,13 @@
-- @field #boolean checkforfriendlies If true, do not activate a SAM installation if a friendly aircraft is in firing range.
-- @field #table FilterZones Table of Core.Zone#ZONE Zones Consider SAM groups in this zone(s) only for this MANTIS instance, must be handed as #table of Zone objects.
-- @field #boolean SmokeDecoy If true, smoke short range SAM units as decoy if a plane is in firing range.
-- @field #number SmokeDecoyColor Color to use, defaults to SMOKECOLOR.White
-- @field #number SmokeDecoyColor Color to use, defaults to SMOKECOLOR.White.
-- @field #number checkcounter Counter for SAM Table refreshes.
-- @field #number DLinkCacheTime Seconds after which cached contacts in DLink will decay.
-- @field #boolean logsamstatus Log SAM status in dcs.log every cycle if true
-- @field #boolean logsamstatus Log SAM status in dcs.log every cycle if true.
-- @field #boolean DetectAccoustic Set if we can also detect units accousticly.
-- @field #number DetectAccousticRadius We can hear in this range.
-- @field #table DetectAccousticCategories We can hear these categories.
-- @extends Core.Base#BASE
@@ -113,7 +116,7 @@
-- * TOR M2
-- * C-RAM
-- * Silkworm (though strictly speaking this is a surface to ship missile)
-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19
-- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19, SA-21, S-300VM, S-300V4, S-400
-- * From IDF mod: STUNNER IDFA, TAMIR IDFA (Note all caps!)
-- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2, SAMP/T Block 1, SAMP/T Block 1INT, SAMP/T Block2
-- * Other Mods: Nike
@@ -280,7 +283,7 @@
MANTIS = {
ClassName = "MANTIS",
name = "mymantis",
version = "0.9.34",
version = "0.9.42",
SAM_Templates_Prefix = "",
SAM_Group = nil,
EWR_Templates_Prefix = "",
@@ -331,6 +334,9 @@ MANTIS = {
checkcounter = 1,
DLinkCacheTime = 120,
logsamstatus = false,
DetectAccoustic = false,
DetectAccousticRadius = 2000,
DetectAccousticCategories = {Unit.Category.HELICOPTER},
}
--- Advanced state enumerator
@@ -395,10 +401,14 @@ MANTIS.SamData = {
["SA-17"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17" },
["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"},
["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"},
["SA-21"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="92N6E"},
["S-300VM"] = { Range=200, Blindspot=5, Height=30, Type="Long" , Radar="9S32M"},
["S-300V4"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="9S32M"},
["S-400"] = { Range=250, Blindspot=5, Height=27, Type="Long" , Radar="92N6E"},
["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" },
["TAMIR IDFA"] = { Range=20, Blindspot=0.6, Height=12.3, Type="Short", Radar="IRON_DOME_LN" },
["STUNNER IDFA"] = { Range=250, Blindspot=1, Height=45, Type="Long", Radar="DAVID_SLING_LN" },
["NIKE"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" },
["Nike"] = { Range=155, Blindspot=6, Height=30, Type="Long", Radar="HIPAR" },
["Dog Ear"] = { Range=11, Blindspot=0, Height=9, Type="Point", Radar="Dog Ear", Point="true" },
-- CH Added to DCS core 2.9.19.x
["Pantsir S1"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1" , Point="true" },
@@ -524,7 +534,7 @@ do
--@param #string samprefix Prefixes for the SAM groups from the ME, e.g. all groups starting with "Red Sam..."
--@param #string ewrprefix Prefixes for the EWR groups from the ME, e.g. all groups starting with "Red EWR..."
--@param #string hq Group name of your HQ (optional)
--@param #string coalition Coalition side of your setup, e.g. "blue", "red" or "neutral"
--@param #string Coalition Coalition side of your setup, e.g. "blue", "red" or "neutral"
--@param #boolean dynamic Use constant (true) filtering or just filter once (false, default) (optional)
--@param #string awacs Group name of your Awacs (optional)
--@param #boolean EmOnOff Make MANTIS switch Emissions on and off instead of changing the alarm state between RED and GREEN (optional)
@@ -548,7 +558,7 @@ do
-- mybluemantis = MANTIS:New("bluemantis","Blue SAM","Blue EWR",nil,"blue",false,"Blue Awacs")
-- mybluemantis:Start()
--
function MANTIS:New(name,samprefix,ewrprefix,hq,coalition,dynamic,awacs, EmOnOff, Padding, Zones)
function MANTIS:New(name,samprefix,ewrprefix,hq,Coalition,dynamic,awacs, EmOnOff, Padding, Zones)
-- Inherit everything from BASE class.
@@ -561,12 +571,14 @@ do
-- DONE: Treat Awacs separately, since they might be >80km off site
-- DONE: Allow tables of prefixes for the setup
-- DONE: Auto-Mode with range setups for various known SAM types.
-- DONE: Added reaction on HIT and UNIT LOST events.
self.name = name or "mymantis"
self.SAM_Templates_Prefix = samprefix or "Red SAM"
self.EWR_Templates_Prefix = ewrprefix or "Red EWR"
self.HQ_Template_CC = hq or nil
self.Coalition = coalition or "red"
self.Coalition = Coalition or "red"
self.coalition = Coalition == "blue" and coalition.side.BLUE or coalition.side.RED
self.SAM_Table = {}
self.SAM_Table_Long = {}
self.SAM_Table_Medium = {}
@@ -716,6 +728,8 @@ do
self:AddTransition("*", "SeadSuppressionStart", "*") -- SEAD has switched off one group.
self:AddTransition("*", "SeadSuppressionEnd", "*") -- SEAD has switched on one group.
self:AddTransition("*", "SeadSuppressionPlanned", "*") -- SEAD has planned a suppression.
self:AddTransition("*", "SAMUnitHit", "*") -- A SAM unit was hit
self:AddTransition("*", "SAMUnitLost", "*") -- A SAM Unit was lost
self:AddTransition("*", "Stop", "Stopped") -- Stop FSM.
------------------------
@@ -826,6 +840,24 @@ do
-- @param Wrapper.Group#GROUP Group The suppressed GROUP object
-- @param #string Name Name of the suppressed group
--- On After "SAMUnitHit" event. A SAM Unit was hit.
-- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd
-- @param #MANTIS self
-- @param #string From The From State
-- @param #string Event The Event
-- @param #string To The To State
-- @param Wrapper.Group#GROUP Group The GROUP of the hit UNIT object
-- @param #string Name Name of the suppressed group
--- On After "SAMUnitLoast" event. A SAM Unit was lost.
-- @function [parent=#MANTIS] OnAfterSeadSuppressionEnd
-- @param #MANTIS self
-- @param #string From The From State
-- @param #string Event The Event
-- @param #string To The To State
-- @param Wrapper.Group#GROUP Group The GROUP of the lost UNIT object
-- @param #string Name Name of the suppressed group
return self
end
@@ -833,6 +865,138 @@ do
-- MANTIS helper functions
-----------------------------------------------------------------------
--- Set to accept accoustic detection. Set this *before* MANTIS starts!
-- @param #MANTIS self
-- @param #number Radius Radius in which we can "hear" units. Defaults to 2000 meters.
-- @param #table UnitCategories Set what Unit Categories we can "hear". Defaults to `{Unit.Category.HELICOPTER}`
-- @return #MANTIS self
function MANTIS:SetAccousticDetectionOn(Radius,UnitCategories)
self.DetectAccoustic = true
self.DetectAccousticRadius = Radius or 2000
self.DetectAccousticCategories = UnitCategories or {Unit.Category.HELICOPTER}
return self
end
--- Switch off accoustic detection.
-- @param #MANTIS self
-- @return #MANTIS self
function MANTIS:SetAccousticDetectionOff()
self.DetectAccoustic = false
return self
end
--- [Internal] Function to manage hits on SAM units
-- @param #MANTIS self
-- @param Core.Event#EVENTDATA EventData The EVENT data
-- @return #MANTIS self
function MANTIS:_EventHandler(EventData)
self:T(self.lid .. "_EventHandler")
local function IsOneOfOurs(name)
for _,_name in pairs(self.ewr_templates) do
if string.find(name,_name,1,true) then
return true
end
end
return false
end
local function SwitchSAMOn(Name,Group)
local suppressed = self.SuppressedGroups[Name] or false
if not suppressed and self.SamStateTracker[Name] == "GREEN" then
self.SamStateTracker[Name] = "RED"
if self.UseEmOnOff then
-- DONE: add emissions on/off
Group:EnableEmission(true)
elseif (not self.UseEmOnOff) then
Group:OptionAlarmStateRed()
end
self:__RedState(1,Group)
if self.SmokeDecoy == true then
self:_SmokeUnits(Group)
end
end
end
local coordinate -- Core.Point#COORDINATE
local Name -- #string
local Group -- Wrapper.Group#GROUP
local lasthit = 0
local firsthit = false
local alerton = false
-- Check if we can get a location
local data = EventData -- Core.Event#EVENTDATA
if data.id == EVENTS.Hit then
-- Unit hit, one of ours?
if data.TgtGroupName and IsOneOfOurs(data.TgtGroupName) then
self:T("Unit hit in group: "..data.TgtGroupName)
if data.TgtGroup then
lasthit = data.TgtGroup:GetProperty("MANTIS_LASTHIT")
firsthit = (lasthit==nil) and true or false
if firsthit == true then alerton = true end
if lasthit ~= nil and timer.getTime()-lasthit > self.ShoradTime then alerton = true end
coordinate = data.TgtGroup:GetCoordinate()
Name = data.TgtGroupName
Group = data.TgtGroup
if alerton == true then
self:__SAMUnitHit(1,Group,Name)
SwitchSAMOn(Name,Group)
end
if coordinate and self.debug then
local text = coordinate:ToStringMGRS()
self:I("Location: "..text)
end
end
end
end
if data.id == EVENTS.UnitLost then
if data.IniGroupName and IsOneOfOurs(data.IniGroupName) then
self:T("Unit lost in group: "..data.IniGroupName)
if data.IniGroup then
lasthit = data.IniGroup:GetProperty("MANTIS_LASTHIT")
firsthit = (lasthit==nil) and true or false
if firsthit == true then alerton = true end
if lasthit ~= nil and timer.getTime()-lasthit > self.ShoradTime then alerton = true end
coordinate = data.IniGroup:GetCoordinate()
Name = data.IniGroupName
Group = data.IniGroup
alerton = true
SwitchSAMOn(Name,Group)
self:__SAMUnitLost(1,Group,Name)
if coordinate and self.debug then
local text = coordinate:ToStringMGRS()
self:I("Location: "..text)
end
end
end
end
if firsthit == true or alerton == true then
Group:SetProperty("MANTIS_LASTHIT",timer.getTime())
end
if coordinate ~= nil and Name ~= nil and Group ~=nil and alerton == true then
if self.ShoradLink then
self:T("Shorad activated for: "..Name)
local Shorad = self.Shorad -- Functional.Shorad#SHORAD
local radius = self.checkradius
local ontime = self.ShoradTime
Shorad:WakeUpShorad(Name, radius, ontime, nil, true)
self:__ShoradActivated(1,Name, radius, ontime)
end
if self.autorelocate and Group then
Group:RelocateGroundRandomInRadius(20,500,true,true,nil,true)
end
end
return self
end
--- [Internal] Function to get the self.SAM_Table
-- @param #MANTIS self
-- @return #table table
@@ -903,13 +1067,111 @@ do
return self
end
--- Add a single reject zone to MANTIS.
-- @param #MANTIS self
-- @param Core.Zone#ZONE Zone The zone to be added
-- @return #MANTIS self
function MANTIS:AddRejectZone(Zone)
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
table.insert(self.RejectZones,Zone)
self.usezones = true
end
return self
end
--- Add a single accept zone to MANTIS.
-- @param #MANTIS self
-- @param Core.Zone#ZONE Zone The zone to be added
-- @return #MANTIS self
function MANTIS:AddAcceptZone(Zone)
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
table.insert(self.AcceptZones,Zone)
self.usezones = true
end
return self
end
--- Add a single conflict zone to MANTIS.
-- @param #MANTIS self
-- @param Core.Zone#ZONE Zone The zone to be added
-- @return #MANTIS self
function MANTIS:AddConflictZone(Zone)
if Zone and Zone:IsInstanceOf("ZONE_BASE") then
table.insert(self.ConflictZones,Zone)
self.usezones = true
end
return self
end
--- Function to set corridor zones.
-- @param #MANTIS self
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
-- @return #MANTIS self
function MANTIS:SetCorridorZones(CorridorZones)
self:T(self.lid .. "SetCorridorZones")
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
self.corridorzones = CorridorZones
self.usecorridors = true
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
self.corridorzones:AddZone(CorridorZones)
self.usecorridors = true
end
if self.intelset then
for _,_intel in pairs(self.intelset) do
_intel:SetCorridorZones(self.corridorzones)
end
end
return self
end
--- Function to add one corridor zone.
-- @param #MANTIS self
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
-- @return #MANTIS self
function MANTIS:AddCorridorZone(CorridorZone)
self:T(self.lid .. "AddCorridorZone")
self:SetCorridorZones(CorridorZone)
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #MANTIS self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #MANTIS self
function MANTIS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
if self.intelset then
for _,_intel in pairs(self.intelset) do
_intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #MANTIS self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #MANTIS self
function MANTIS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
self.corridorfloor = Floor
self.corridorceiling = Ceiling
if self.intelset then
for _,_intel in pairs(self.intelset) do
_intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
return self
end
--- Function to set the detection radius of the EWR in meters. (Deprecated, SAM range is used)
-- @param #MANTIS self
-- @param #number radius Radius of the EWR detection zone
function MANTIS:SetEWRRange(radius)
self:T(self.lid .. "SetEWRRange")
--local radius = radius or 80000
-- self.acceptrange = radius
return self
end
@@ -1325,11 +1587,12 @@ do
return inzone
end
--- [Internal] Function to prefilter height based
--- [Internal] Function to prefilter height based and check for Helo activity.
-- @param #MANTIS self
-- @param #number height
-- @param Core.Point#COORDINATE SamCoordinate
-- @return #table set
function MANTIS:_PreFilterHeight(height)
function MANTIS:_PreFilterHeight(height,SamCoordinate)
self:T(self.lid.."_PreFilterHeight")
local set = {}
local dlink = self.Detection -- Ops.Intel#INTEL_DLINK
@@ -1338,8 +1601,22 @@ do
local contact = _contact -- Ops.Intel#INTEL.Contact
local grp = contact.group -- Wrapper.Group#GROUP
if grp:IsAlive() then
if grp:GetHeight(true) < height then
local coord = grp:GetCoordinate()
local coord = grp:GetCoordinate()
local dist = 0
local include = true
if grp:IsGround() then include = false end
if grp:GetCoalition() == self.coalition then include = false end
if coord and SamCoordinate and grp:IsHelicopter() then
dist = coord:Get2DDistance(SamCoordinate) or 0
if dist > self.ShoradActDistance then include = false end -- we do not want long range shooting at helos
end
if self.debug then
local text = "Looking at Group: "..grp:GetName() or "N/A"
text = text .. " Include = "..tostring(include)
MESSAGE:New(text,10,"MANTIS"):ToAllIf(self.verbose):ToLog()
end
local grpalt = grp:GetHeight(true)
if grpalt < height and grpalt > 10 and include == true then
table.insert(set,coord)
end
end
@@ -1363,7 +1640,7 @@ do
local set = dectset
if dlink then
-- DEBUG
set = self:_PreFilterHeight(height)
set = self:_PreFilterHeight(height,samcoordinate)
end
--self.friendlyset -- Core.Set#SET_GROUP
if self.checkforfriendlies == true and self.friendlyset == nil then
@@ -1444,14 +1721,30 @@ do
self.intelset = {}
local IntelOne = INTEL:New(groupset,self.Coalition,self.name.." IntelOne")
--IntelOne:SetClusterAnalysis(true,true)
--IntelOne:SetClusterRadius(5000)
local IntelOne = INTEL:New(groupset,self.coalition,self.name.." IntelOne")
IntelOne.DetectAccoustic = self.DetectAccoustic
IntelOne.DetectAccousticRadius = self.DetectAccousticRadius or 2000
IntelOne.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER}
--IntelOne:SetClusterAnalysis(true,true,true)
if self.usecorridors == true then
IntelOne:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
IntelOne:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
IntelOne:Start()
local IntelTwo = INTEL:New(samset,self.Coalition,self.name.." IntelTwo")
--IntelTwo:SetClusterAnalysis(true,true)
--IntelTwo:SetClusterRadius(5000)
local IntelTwo = INTEL:New(samset,self.coalition,self.name.." IntelTwo")
IntelTwo.DetectAccoustic = self.DetectAccoustic
IntelTwo.DetectAccousticRadius = self.DetectAccousticRadius or 2000
IntelTwo.DetectAccousticUnitTypes = self.DetectAccousticCategories or {Unit.Category.HELICOPTER}
--IntelTwo:SetClusterAnalysis(true,true,true)
if self.usecorridors == true then
IntelTwo:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
IntelTwo:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
IntelTwo:Start()
local CacheTime = self.DLinkCacheTime or 120
@@ -1780,6 +2073,27 @@ do
self.ShoradLink = false
return self
end
--- [Internal] Function to smoke a group in decoy.
-- @param #MANTIS self
-- @param Wrapper.Group#GROUP Group
-- @return #MANTIS self
function MANTIS:_SmokeUnits(Group)
self:T("Smoking")
local LastSmoketime=Group:GetProperty("MANTIS_LASTSMOKE_TIME") or 0
local TNow = timer.getTime()
if TNow - LastSmoketime > 290 then -- Smoking lasts 5 minutes
Group:SetProperty("MANTIS_LASTSMOKE_TIME",TNow)
local units = Group:GetUnits() or {}
local smoke = self.SmokeDecoyColor or SMOKECOLOR.White
for _,unit in pairs(units) do
if unit and unit:IsAlive() then
unit:GetCoordinate():Smoke(smoke)
end
end
end
return self
end
-----------------------------------------------------------------------
-- MANTIS main functions
@@ -1807,7 +2121,7 @@ do
local radius = _data[3]
local height = _data[4]
local blind = _data[5] * 1.25 + 1
local shortsam = (_data[6] == MANTIS.SamType.SHORT) and true or false
local shortsam = (_data[6] ~= MANTIS.SamType.LONG) and true or false
if not shortsam then
shortsam = (_data[6] == MANTIS.SamType.POINT) and true or false
end
@@ -1834,18 +2148,11 @@ do
end
if self.SamStateTracker[name] ~= "RED" and switch then
self:__RedState(1,samgroup)
self.SamStateTracker[name] = "RED"
end
-- TODO doesn't work
if shortsam == true and self.SmokeDecoy == true then
-- DONE Restrict on Distance
if shortsam == true and self.SmokeDecoy == true and Distance < self.DetectAccousticRadius*1.5 then
self:T("Smoking")
local units = samgroup:GetUnits() or {}
local smoke = self.SmokeDecoyColor or SMOKECOLOR.White
for _,unit in pairs(units) do
if unit and unit:IsAlive() then
unit:GetCoordinate():Smoke(smoke)
end
end
self:_SmokeUnits(samgroup)
end
-- link in to SHORAD if available
-- DONE: Test integration fully
@@ -2030,13 +2337,9 @@ do
else
self.Detection = self:StartIntelDetection()
end
--[[
if self.advAwacs and not self.automode then
self.AWACS_Detection = self:StartAwacsDetection()
end
--]]
if self.autoshorad then
self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.coalition,self.UseEmOnOff)
self.Shorad = SHORAD:New(self.name.."-SHORAD","SHORAD",self.SAM_Group,self.ShoradActDistance,self.ShoradTime,self.Coalition,self.UseEmOnOff,self.SmokeDecoy,self.SmokeDecoyColor)
self.Shorad:SetDefenseLimits(80,95)
self.ShoradLink = true
self.Shorad.Groupset=self.ShoradGroupSet
@@ -2045,6 +2348,10 @@ do
if self.shootandscoot and self.SkateZones and self.Shorad then
self.Shorad:AddScootZones(self.SkateZones,self.SkateNumber or 3,self.ScootRandom,self.ScootFormation)
end
self:HandleEvent(EVENTS.Hit,self._EventHandler)
self:HandleEvent(EVENTS.UnitLost,self._EventHandler)
self:__Status(-math.random(1,10))
return self
end
@@ -100,7 +100,7 @@ PSEUDOATC.id="PseudoATC | "
--- PSEUDOATC version.
-- @field #number version
PSEUDOATC.version="0.10.5"
PSEUDOATC.version="0.10.6"
-----------------------------------------------------------------------------------------------------------------------------------------
@@ -579,6 +579,9 @@ function PSEUDOATC:MenuAirports(GID,UID)
-- Create menu reporting commands
missionCommands.addCommandForGroup(GID, "Weather Report", submenu, self.ReportWeather, self, GID, UID, pos, name)
missionCommands.addCommandForGroup(GID, "Request BR", submenu, self.ReportBR, self, GID, UID, pos, name)
if self.radios then
missionCommands.addCommandForGroup(GID, "Radios", submenu, self.ReportRadios, self, GID, UID, pos, name)
end
-- Debug message.
self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, GID))
@@ -705,7 +708,30 @@ function PSEUDOATC:ReportWeather(GID, UID, position, location)
end
--- Report absolute bearing and range form player unit to airport.
--- Report airport radio information.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player.
-- @param Core.Point#COORDINATE position Coordinate of the airport.
-- @param #string location Name of the airport.
function PSEUDOATC:ReportRadios(GID, UID, position, location)
self:F({GID=GID, UID=UID, position=position, location=location})
if self.radios then
local Text=""
local radio = self.radios:GetClosestRadio(position,9) --Navigation.Radios#RADIOS.Radio
if radio then
Text=self.radios:_GetMarkerText(radio)
else
Text=self.group[GID].player[UID].playername..", no radio information found!"
end
-- Send message
self:_DisplayMessageToGroup(self.group[GID].player[UID].unit, Text, self.mdur, true)
end
return self
end
--- Report absolute bearing and range from player unit to airport.
-- @param #PSEUDOATC self
-- @param #number GID Group id of player unit.
-- @param #number UID Unit id of player.
@@ -979,3 +1005,22 @@ function PSEUDOATC:_myname(unitname)
return string.format("%s (%s)", csign, pname)
end
--- Returns a string which consits of this callsign and the player name.
-- @param #PSEUDOATC self
-- @param #string path Path to map data, e.g. `[[<DCS_Install_Directory>\Mods\terrains\<Map_Name>\Radio.lua]]` (replace with correct path).
-- Needs `lfs` and `io` to be desanitized in the `MissionScripting.lua` in `<DCS_Install_Directory>\Scripts`
-- @return #PSEUDOATC self
-- @usage
--
-- mypseudoatc:SetUsingRadioInformationFromMap([[C:\Program Files\Eagle Dynamics\DCS World.Openbeta\Mods\terrains\Caucasus\Radio.lua]])
--
function PSEUDOATC:SetUsingRadioInformationFromMap(path)
if RADIOS and lfs and io then
self.radios = RADIOS:NewFromFile(path)
else
self:E("PSEUDOATC:SetUsingRadioInformationFromMap Needs `lfs`and `io` to be desanitized in the `MissionScripting.lua` in `<DCS_Install_Directory>/Scripts`")
end
return self
end
+16 -14
View File
@@ -5584,7 +5584,7 @@ end
-- @param #string dest Name of the destination airport.
function RAT:_ATCAddFlight(name, dest)
-- Debug info
BASE:I(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest))
BASE:T(RAT.id..string.format("ATC %s: Adding flight %s with destination %s.", dest, name, dest))
-- Create new flight
local flight={} --#RAT.AtcFlight
@@ -5603,7 +5603,7 @@ end
function RAT._ATCDelFlight(t,entry)
for k,_ in pairs(t) do
if k==entry then
BASE:I(RAT.id..string.format("Removing flight %s from queue", entry))
BASE:T(RAT.id..string.format("Removing flight %s from queue", entry))
t[entry]=nil
end
end
@@ -5614,7 +5614,7 @@ end
-- @param #string name Group name of the flight.
-- @param #number time Time the fight first registered.
function RAT._ATCRegisterFlight(name, time)
BASE:I(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name))
BASE:T(RAT.id..string.format("Flight %s registered at ATC for landing clearance.", name))
RAT.ATC.flight[name].Tarrive=time
RAT.ATC.flight[name].holding=0
end
@@ -5649,15 +5649,16 @@ function RAT._ATCStatus()
-- Aircraft is holding.
local text=string.format("ATC %s: Flight %s is holding for %i:%02d. %s.", dest, name, hold/60, hold%60, busy)
BASE:I(RAT.id..text)
BASE:T(RAT.id..text)
elseif hold==RAT.ATC.onfinal then
-- Aircarft is on final approach for landing.
local Tfinal=Tnow-flight.Tonfinal
local Tonfinal = flight.Tonfinal or timer.getTime()-1
local Tfinal=Tnow-Tonfinal
local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60)
BASE:I(RAT.id..text)
BASE:T(RAT.id..text)
elseif hold==RAT.ATC.unregistered then
@@ -5711,13 +5712,13 @@ function RAT._ATCCheck()
-- Debug message.
local text=string.format("ATC %s: Flight %s runway is busy. You are #%d of %d in landing queue. Your holding time is %i:%02d.",
airportname, flightname, qID, nqueue, flight.holding/60, flight.holding%60)
BASE:I(RAT.id..text)
BASE:T(RAT.id..text)
else
local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.",
airportname, flightname, flight.holding/60, flight.holding%60)
BASE:I(RAT.id..text)
BASE:T(RAT.id..text)
-- Clear flight for landing.
RAT._ATCClearForLanding(airportname, flightname)
@@ -5772,7 +5773,7 @@ function RAT._ATCClearForLanding(airportname, flightname)
-- Debug message.
BASE:I(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname))
BASE:T(RAT.id..string.format("ATC %s: Flight %s cleared for landing", airportname, flightname))
if string.find(flightname,"#") then
flightname = string.match(flightname,"^(.+)#")
@@ -5799,8 +5800,9 @@ function RAT._ATCFlightLanded(name)
-- Times for holding and final approach.
local Tnow=timer.getTime()
local Tfinal=Tnow-flight.Tonfinal
local Thold=flight.Tonfinal-flight.Tarrive
local Tonfinal = flight.Tonfinal or timer.getTime()-1
local Tfinal=Tnow-Tonfinal
local Thold=Tonfinal-flight.Tarrive
local airport=RAT.ATC.airport[dest] --#RAT.AtcAirport
@@ -5823,9 +5825,9 @@ function RAT._ATCFlightLanded(name)
local TrafficPerHour=airport.traffic/(timer.getTime()-RAT.ATC.T0)*3600
-- Debug info
BASE:I(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60))
BASE:I(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal))
BASE:I(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour))
BASE:T(RAT.id..string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60))
BASE:T(RAT.id..string.format("ATC %s: Number of flights still on final %d.", dest, airport.Nonfinal))
BASE:T(RAT.id..string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, airport.traffic, TrafficPerHour))
if string.find(name,"#") then
name = string.match(name,"^(.+)#")
+20 -6
View File
@@ -109,7 +109,6 @@
-- @field Core.Menu#MENU_MISSION menuF10root Specific user defined root F10 menu.
-- @field #number ceilingaltitude Range ceiling altitude in ft MSL. Aircraft above this altitude are not considered to be in the range. Default is 20000 ft.
-- @field #boolean ceilingenabled Range has a ceiling and is not unlimited. Default is false.
-- @extends Core.Fsm#FSM
--- *Don't only practice your art, but force your way into its secrets; art deserves that, for it and knowledge can raise man to the Divine.* - Ludwig van Beethoven
@@ -1271,6 +1270,9 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume,
self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE)
self.controlmsrs:SetLabel("RANGEC")
self.controlmsrs:SetVolume(Volume or 1.0)
if self.rangezone then
self.controlmsrs:SetCoordinate(self.rangezone:GetCoordinate())
end
self.controlsrsQ = MSRSQUEUE:New("CONTROL")
self.instructmsrs=MSRS:New(PathToSRS or MSRS.path, Frequency or 305, Modulation or radio.modulation.AM)
@@ -1278,6 +1280,9 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume,
self.instructmsrs:SetCoalition(Coalition or coalition.side.BLUE)
self.instructmsrs:SetLabel("RANGEI")
self.instructmsrs:SetVolume(Volume or 1.0)
if self.rangezone then
self.instructmsrs:SetCoordinate(self.rangezone:GetCoordinate())
end
self.instructsrsQ = MSRSQUEUE:New("INSTRUCT")
if PathToGoogleKey then
@@ -1316,8 +1321,13 @@ function RANGE:SetSRSRangeControl( frequency, modulation, voice, culture, gender
self.rangecontrol = true
if relayunitname then
local unit = UNIT:FindByName(relayunitname)
local Coordinate = unit:GetCoordinate()
self.rangecontrolrelayname = relayunitname
if unit then
local Coordinate = unit:GetCoordinate()
self.rangecontrolrelayname = relayunitname
self.controlmsrs:SetCoordinate(Coordinate)
else
MESSAGE:New("RANGE: Control Relay Unit "..relayunitname.." not found!",15,"ERROR"):ToAllIf(self.Debug):ToLog()
end
end
return self
end
@@ -1345,9 +1355,13 @@ function RANGE:SetSRSRangeInstructor( frequency, modulation, voice, culture, gen
self.instructor = true
if relayunitname then
local unit = UNIT:FindByName(relayunitname)
local Coordinate = unit:GetCoordinate()
self.instructmsrs:SetCoordinate(Coordinate)
self.instructorrelayname = relayunitname
if unit then
local Coordinate = unit:GetCoordinate()
self.instructmsrs:SetCoordinate(Coordinate)
self.instructorrelayname = relayunitname
else
MESSAGE:New("RANGE: Instructor Relay Unit "..relayunitname.." not found!",15,"ERROR"):ToAllIf(self.Debug):ToLog()
end
end
return self
end
+161 -45
View File
@@ -230,8 +230,10 @@ SCORING = {
ClassID = 0,
Players = {},
AutoSave = true,
version = "1.18.4",
version = "1.18.5",
ScoringScenery = nil, -- Core.Set#SET_SCENERY
SceneryHitsInZone = false,
LoadSave = false,
}
local _SCORINGCoalition = {
@@ -250,15 +252,16 @@ local _SCORINGCategory = {
--- Creates a new SCORING object to administer the scoring achieved by players.
-- @param #SCORING self
-- @param #string GameName The name of the game. This name is also logged in the CSV score file.
-- @param #string SavePath (Optional) Path where to save the CSV file, defaults to your **<User>\\Saved Games\\DCS\\Logs** folder.
-- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off.
-- @param #string SavePath (Optional) Path where to save the CSV files, defaults to your **<User>\\Saved Games\\DCS\\Logs** folder. See next two options.
-- @param #boolean AutoSave (Optional) If passed as `false`, then swith autosave off. This stores a detailed table which will never be loaded again by SCORING (for e.g. Discord purposes).
-- @param #boolean LoadSave (Optional) If passed as `true` save summary scores per player, and load at restart of the mission.
-- @return #SCORING self
-- @usage
--
-- -- Define a new scoring object for the mission Gori Valley.
-- ScoringObject = SCORING:New( "Gori Valley" )
--
function SCORING:New( GameName, SavePath, AutoSave )
function SCORING:New( GameName, SavePath, AutoSave, LoadSave )
-- Inherits from BASE
local self = BASE:Inherit( self, BASE:New() ) -- #SCORING
@@ -266,7 +269,7 @@ function SCORING:New( GameName, SavePath, AutoSave )
if GameName then
self.GameName = GameName
else
error( "A game name must be given to register the scoring results" )
error( "A game name must be given to register the scoring results!" )
end
-- Additional Object scores
@@ -301,6 +304,12 @@ function SCORING:New( GameName, SavePath, AutoSave )
self.penaltyoncoalitionchange = true
self:SetDisplayMessagePrefix()
self.SceneryHitsInZone = false
if LoadSave then
self.LoadSave = LoadSave
end
-- Event handlers
self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
@@ -328,11 +337,61 @@ function SCORING:New( GameName, SavePath, AutoSave )
end
self:I("SCORING "..tostring(GameName).." started! v"..self.version)
if LoadSave == true then
self:_LoadPlayerSummaryScore()
end
return self
end
--- [Internal] Helper to load scores from disk at scoring start
-- @param #SCORING self
-- @return #SCORING self
function SCORING:_LoadPlayerSummaryScore()
if lfs and io and self.LoadSave == true then
local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]]
local filename = self.GameName or "PlayerScoresSummary"
filename = filename..".csv"
if UTILS.CheckFileExists(path,filename) then
local ok, data = UTILS.LoadFromFile(path,filename)
-- Playername;;Score;;Penalty
table.remove(data,1)
for _,_data in pairs(data) do
local line = UTILS.Split(_data,";;")
local playername = tostring(line[1])
local score = tonumber(line[2])
local penalty = tonumber(line[3])
self:I(string.format("Player %s Score %d Penalty %d",playername,score,penalty))
local PlayerData = self.Players[playername]
if not PlayerData then
PlayerData = {}
PlayerData.Hit = {}
PlayerData.Destroy = {}
PlayerData.Goals = {}
PlayerData.Goals[self.GameName] = {Score = score, Penalty = penalty}
PlayerData.Mission = {}
PlayerData.HitPlayers = {}
PlayerData.Score = score
PlayerData.Penalty = penalty
PlayerData.PenaltyCoalition = 0
PlayerData.PenaltyWarning = 0
self.Players[playername] = PlayerData
else
PlayerData.Score = score
PlayerData.Penalty =penalty
self.Players[playername] = PlayerData
PlayerData.Goals[self.GameName] = {Score = score, Penalty = penalty}
end
end
end
end
return self
end
--- Set a prefix string that will be displayed at each scoring message sent.
-- @param #SCORING self
-- @param #string DisplayMessagePrefix (Default="Scoring: ") The scoring prefix string.
@@ -579,6 +638,22 @@ function SCORING:AddZoneScore( ScoreZone, Score )
return self
end
--- Allow Scenery hits in Zones to count (no specific(!) scenery targets). NOTE - Allowing this can spam your scoring display!
-- @param #SCORING self
-- @return #SCORING self
function SCORING:EnableSceneryHitsinZones()
self.SceneryHitsInZone = true
return self
end
--- Disallow Scenery hits in Zones to count (no specific(!) scenery targets).
-- @param #SCORING self
-- @return #SCORING self
function SCORING:DisableSceneryHitsinZones()
self.SceneryHitsInZone = false
return self
end
--- Add a @{Core.Set#SET_ZONE} to define additional scoring when any object is destroyed in that zone.
-- Note that if a @{Core.Zone} with the same name is already within the scoring added, the @{Core.Zone} (type) and Score will be replaced!
-- This allows for a dynamic destruction zone evolution within your mission.
@@ -877,6 +952,20 @@ function SCORING:AddGoalScorePlayer( PlayerName, GoalTag, Text, Score )
-- PlayerName can be nil, if the Unit with the player crashed or due to another reason.
if PlayerName then
local PlayerData = self.Players[PlayerName]
if not PlayerData then
PlayerData = {}
PlayerData.Goals = {}
PlayerData.Hit = {}
PlayerData.Destroy = {}
PlayerData.Goals = {}
PlayerData.Mission = {}
PlayerData.HitPlayers = {}
PlayerData.Score = 0
PlayerData.Penalty = 0
PlayerData.PenaltyCoalition = 0
PlayerData.PenaltyWarning = 0
self.Players[PlayerName] = PlayerData
end
PlayerData.Goals[GoalTag] = PlayerData.Goals[GoalTag] or { Score = 0 }
PlayerData.Goals[GoalTag].Score = PlayerData.Goals[GoalTag].Score + Score
@@ -1153,7 +1242,7 @@ function SCORING:_EventOnHit( Event )
TargetType = "Scenery"
TargetSceneryObject = TargetUNIT
self:T("***** Target is Scenery and TargetUNIT is SCENERY object!")
UTILS.PrintTableToLog(TargetSceneryObject)
--UTILS.PrintTableToLog(TargetSceneryObject)
end
TargetUnitCoalition = _SCORINGCoalition[TargetCoalition]
@@ -1374,17 +1463,18 @@ function SCORING:_EventOnHit( Event )
-- Player contains the score data from self.Players[WeaponPlayerName]
local PlayerName = Event.IniPlayerName or "Ghost"
local Player = self.Players[PlayerName]
Player.Score = Player.Score + Score
Player.Score = Player.Score + self.ScoreIncrementOnHit
MESSAGE:NewType( self.DisplayMessagePrefix .. "hit in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "HIT_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Zone", TargetUnitType )
end
if Player then
Player.Score = Player.Score + Score
Player.Score = Player.Score + self.ScoreIncrementOnHit
MESSAGE:NewType( self.DisplayMessagePrefix .. "hit in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "HIT_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Zone", TargetUnitType )
end
end
end
end
@@ -1567,22 +1657,24 @@ function SCORING:_EventOnDeadOrCrash( Event )
end
else
-- Check if there are Zones where the destruction happened.
for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do
self:F( { ScoringZone = ScoreZoneData } )
local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
local Score = ScoreZoneData.Score
if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
Player.Score = Player.Score + Score
TargetDestroy.Score = TargetDestroy.Score + Score
MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
Destroyed = true
if self.SceneryHitsInZone == true then
-- Check if there are Zones where the destruction happened.
for ZoneName, ScoreZoneData in pairs( self.ScoringZones ) do
self:F( { ScoringZone = ScoreZoneData } )
local ScoreZone = ScoreZoneData.ScoreZone -- Core.Zone#ZONE_BASE
local Score = ScoreZoneData.Score
if ScoreZone:IsVec2InZone( TargetUnit:GetVec2() ) then
Player.Score = Player.Score + Score
TargetDestroy.Score = TargetDestroy.Score + Score
MESSAGE:NewType( self.DisplayMessagePrefix .. "Scenery destroyed in zone '" .. ScoreZone:GetName() .. "'." ..
"Player '" .. PlayerName .. "' receives an extra " .. Score .. " points! " .. "Total: " .. Player.Score - Player.Penalty,
MESSAGE.Type.Information )
:ToAllIf( self:IfMessagesZone() and self:IfMessagesToAll() )
:ToCoalitionIf( InitCoalition, self:IfMessagesZone() and self:IfMessagesToCoalition() )
self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
Destroyed = true
end
end
end
end
@@ -1926,9 +2018,12 @@ end
--- Report all players score
-- @param #SCORING self
-- @param Wrapper.Group#GROUP PlayerGroup The player group.
function SCORING:ReportScoreAllSummary( PlayerGroup )
-- @param #boolean JustScore If this is true, return just a table with playernames and overall scores.
-- @return #table ReportTable Table returned if JustScore is true.
function SCORING:ReportScoreAllSummary( PlayerGroup, JustScore )
local PlayerMessage = ""
local ReportTable = {}
self:T( { "Summary Score Report of All Players", Players = self.Players } )
@@ -1960,20 +2055,28 @@ function SCORING:ReportScoreAllSummary( PlayerGroup )
local PlayerScore = ScoreHits + ScoreDestroys + ScoreCoalitionChanges + ScoreGoals + ScoreMissions
local PlayerPenalty = PenaltyHits + PenaltyDestroys + PenaltyCoalitionChanges + PenaltyGoals + PenaltyMissions
PlayerMessage =
string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )",
PlayerName,
PlayerScore - PlayerPenalty,
PlayerScore,
PlayerPenalty
)
MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup )
if JustScore~=true then
PlayerMessage =
string.format( "Player '%s' Score = %d ( %d Score, -%d Penalties )",
PlayerName,
PlayerScore - PlayerPenalty,
PlayerScore,
PlayerPenalty
)
MESSAGE:NewType( PlayerMessage, MESSAGE.Type.Overview ):ToGroup( PlayerGroup )
else
ReportTable[PlayerName] = {["Score"]=PlayerScore,["Penalty"]=PlayerPenalty}
end
end
end
return ReportTable
end
--- Opens a score CSV file to log the scores.
-- @param #SCORING self
-- @param #number sSeconds
-- @return #string ClockString
function SCORING:SecondsToClock( sSeconds )
local nSeconds = sSeconds
if nSeconds == 0 then
@@ -2101,6 +2204,19 @@ function SCORING:ScoreCSV( PlayerName, TargetPlayerName, ScoreType, ScoreTimes,
self.CSVFile:write( "\n" )
end
if lfs and io and self.LoadSave == true then
local path = self.AutoSavePath or lfs.writedir() .. [[Logs\]]
local filename = self.GameName or "PlayerScoresSummary"
filename = filename..".csv"
local data = self:ReportScoreAllSummary("",true)
local text = "-- Playername;;Score;;Penalty\n"
for _playername,_data in pairs(data or {}) do
text = text..string.format("%s;;%d;;%d\n")
end
UTILS.SaveToFile(path,filename,text)
end
end
--- Close CSV file
+47 -5
View File
@@ -48,7 +48,9 @@
-- @field #number minscootdist Min distance of the next zone
-- @field #number maxscootdist Max distance of the next zone
-- @field #boolean scootrandomcoord If true, use a random coordinate in the zone and not the center
-- @field #string scootformation Formation to take for scooting, e.g. "Vee" or "Cone"
-- @field #string scootformation Formation to take for scooting, e.g. "Vee" or "Cone"
-- @field #boolean SmokeDecoy = false,
-- @field #number SmokeDecoyColor = SMOKECOLOR.White
-- @extends Core.Base#BASE
@@ -114,7 +116,9 @@ SHORAD = {
SkateZones = nil,
minscootdist = 100,
maxscootdist = 3000,
scootrandomcoord = false,
scootrandomcoord = false,
SmokeDecoy = false,
SmokeDecoyColor = SMOKECOLOR.White
}
-----------------------------------------------------------------------
@@ -161,8 +165,10 @@ do
-- @param #number ActiveTimer Determines how many seconds the systems stay on red alert after wake-up call
-- @param #string Coalition Coalition, i.e. "blue", "red", or "neutral"
-- @param #boolean UseEmOnOff Use Emissions On/Off rather than Alarm State Red/Green (default: use Emissions switch)
-- @param #boolean SmokeDecoy Throw smoke decoy when getting activated. Defaults to false.
-- @param #number SmokeDecoyColor SMOLECOLOR to use. Defaults to SMOLECOLOR.White
-- @return #SHORAD self
function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff)
function SHORAD:New(Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition, UseEmOnOff, SmokeDecoy, SmokeDecoyColor)
local self = BASE:Inherit( self, FSM:New() )
self:T({Name, ShoradPrefix, Samset, Radius, ActiveTimer, Coalition})
@@ -171,6 +177,7 @@ do
self.name = Name or "MyShorad"
self.Prefixes = ShoradPrefix or "SAM SHORAD"
self.Radius = Radius or 20000
if type(Coalition) == "number" then Coalition = string.lower(UTILS.GetCoalitionName(Coalition)) end
self.Coalition = Coalition or "blue"
self.Samset = Samset or GroupSet
self.ActiveTimer = ActiveTimer or 600
@@ -181,8 +188,15 @@ do
self.DefenseLowProb = 70 -- probability to detect a missile shot, low margin
self.DefenseHighProb = 90 -- probability to detect a missile shot, high margin
self.UseEmOnOff = true -- Decide if we are using Emission on/off (default) or AlarmState red/green
if UseEmOnOff == false then self.UseEmOnOff = UseEmOnOff end
self:I("*** SHORAD - Started Version 0.3.4")
if SmokeDecoy then
self.SmokeDecoy = SmokeDecoy
self.SmokeDecoyColor = SmokeDecoyColor or SMOKECOLOR.White
end
self:I("*** SHORAD - Started Version 0.3.5")
-- Set the string id for output to DCS.log file.
self.lid=string.format("SHORAD %s | ", self.name)
self:_InitState()
@@ -451,6 +465,32 @@ do
return returnname
end
--- Smoke a SHORAD Group
-- @param #SHORAD self
-- @param Wrapper.Group#GROUP Group The Shorad Group to Smoke
-- @return self
function SHORAD:_SmokeUnits(Group)
if self.SmokeDecoy == true then
if Group and Group:IsAlive() then
local units = Group:GetUnits()
for _,_unit in pairs(units) do
local unit = _unit -- Wrapper.Unit#UNIT
if unit and unit:IsAlive() then
local coordinate = unit:GetCoordinate()
if coordinate then
coordinate:SwitchSmokeOffsetOn()
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,1,20)
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,180,20)
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,270,20)
coordinate:Smoke(self.SmokeDecoyColor,Duration,nil,Name,true,90,20)
end
end
end
end
end
return self
end
--- Calculate if the missile shot is detected
-- @param #SHORAD self
-- @return #boolean Returns true for a detection, else false
@@ -537,7 +577,7 @@ do
local text = string.format("Shot at SHORAD %s! Evading!", _group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
self:_SmokeUnits(_group)
--Shoot and Scoot
if self.shootandscoot then
self:__ShootAndScoot(1,_group)
@@ -552,6 +592,7 @@ do
_group:EnableEmission(true)
end
_group:OptionAlarmStateRed()
self:_SmokeUnits(_group)
if self.ActiveGroups[groupname] == nil then -- no timer yet for this group
self.ActiveGroups[groupname] = { Timing = ActiveTimer }
local endtime = timer.getTime() + (ActiveTimer * math.random(75,100) / 100 ) -- randomize wakeup a bit
@@ -684,6 +725,7 @@ do
local ShootingWeapon = EventData.Weapon -- Identify the weapon fired
local ShootingWeaponName = EventData.WeaponName -- return weapon type
-- get firing coalition
if not EventData.IniGroup then return self end
local weaponcoalition = EventData.IniGroup:GetCoalition()
-- get detection probability
if self:_CheckCoalition(weaponcoalition) then --avoid overhead on friendly fire
@@ -4112,10 +4112,10 @@ function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay,
-- Get name of template group.
local templategroupname=group:GetName()
local Descriptors=group:GetUnit(1):GetDesc()
local unit = group:GetUnit(1)
local Descriptors= (unit and unit:IsAlive()) and unit:GetDesc() or {}
local Category=group:GetCategory()
local TypeName=group:GetTypeName()
local TypeName=group:GetTypeName() or "none"
local SpeedMax=group:GetSpeedMax()
local RangeMin=group:GetRange()
local smax,sx,sy,sz=_GetObjectSize(Descriptors)
@@ -6132,6 +6132,7 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol
unit.onboard_num=asset.modex[i]
end
if asset.callsign then
--UTILS.PrintTableToLog(asset.callsign)
unit.callsign=asset.callsign[i]
end
+1
View File
@@ -118,6 +118,7 @@ __Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/PlayerRecce.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Squadron.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/Target.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyGCICAP.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/Ops/EasyA2G.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/AI/AI_Balancer.lua' )
__Moose.Include( MOOSE_DEVELOPMENT_FOLDER..'/Moose/AI/AI_Air.lua' )
@@ -116,6 +116,7 @@ __Moose.Include( 'Ops\\Operation.lua' )
__Moose.Include( 'Ops\\FlightControl.lua' )
__Moose.Include( 'Ops\\PlayerRecce.lua' )
__Moose.Include( 'Ops\\EasyGCICAP.lua' )
__Moose.Include( 'Ops\\EasyA2G.lua' )
__Moose.Include( 'AI\\AI_Balancer.lua' )
__Moose.Include( 'AI\\AI_Air.lua' )
@@ -49,7 +49,7 @@
--
-- A new `RADIOS` object can be created with the @{#RADIOS.NewFromFile}(*radio_lua_file*) function.
--
-- local radios=RADIOS:NewFromFile("<DCS_Install_Directory>\Mods\terrains\<Map_Name>\radio.lua")
-- local radios=RADIOS:NewFromFile("<DCS_Install_Directory>\Mods\terrains\<Map_Name>\Radio.lua")
-- radios:MarkerShow()
--
-- This will load the radios from the `<DCS_Install_Directory>` for the specific map and place markers on the F10 map. This is the first step you should do to ensure that the file
+21
View File
@@ -326,6 +326,27 @@ function AIRWING:AddSquadron(Squadron)
if Squadron:IsStopped() then
Squadron:Start()
end
-- if storage is limited, add the amount of aircraft needed
local airbasename = self:GetAirbaseName()
if airbasename then
local group = GROUP:FindByName(Squadron.templategroup)
local Nunits = 1
local units
if group then units = group:GetUnits() end
if units then Nunits = #units end
local typename = Squadron.aircrafttype or "none"
local NAssets = Squadron.Ngroups * Nunits
local storage = STORAGE:New(airbasename)
--self:T(self.lid.."Adding "..typename.." #"..NAssets)
if storage and storage:IsLimitedAircraft() and typename ~= "none" then
local NInStore = storage:GetItemAmount(typename) or 0
if NAssets > NInStore then
storage:AddItem(typename,NAssets)
end
end
end
return self
end
+1 -1
View File
@@ -3060,7 +3060,7 @@ end
--- **[LEGION, COMMANDER, CHIEF]** Set the repeat delay in seconds after a mission is successful/failed. Only valid if the mission is handled by a LEGION (AIRWING, BRIGADE, FLEET) or higher level.
-- @param #AUFTRAG self
-- @param #number Nrepeat Repeat delay in seconds. Default 1.
-- @param #number RepeatDelay Repeat delay in seconds. Default 1.
-- @return #AUFTRAG self
function AUFTRAG:SetRepeatDelay(RepeatDelay)
self.repeatDelay = RepeatDelay
+58 -1
View File
@@ -509,7 +509,7 @@ do
-- @field #AWACS
AWACS = {
ClassName = "AWACS", -- #string
version = "0.2.73", -- #string
version = "0.2.74", -- #string
lid = "", -- #string
coalition = coalition.side.BLUE, -- #number
coalitiontxt = "blue", -- #string
@@ -2060,6 +2060,55 @@ function AWACS:SetRejectionZone(Zone,Draw)
return self
end
--- Function to set corridor zones.
-- @param #AWACS self
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
-- @return #AWACS self
function AWACS:SetCorridorZones(CorridorZones)
self:T(self.lid .. "SetCorridorZones")
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
self.corridorzones = CorridorZones
self.usecorridors = true
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
self.corridorzones:AddZone(CorridorZones)
self.usecorridors = true
end
return self
end
--- Function to add one corridor zone.
-- @param #AWACS self
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
-- @return #AWACS self
function AWACS:AddCorridorZone(CorridorZone)
self:T(self.lid .. "AddCorridorZone")
self:SetCorridorZones(CorridorZone)
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #AWACS self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #AWACS self
function AWACS:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #AWACS self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #AWACS self
function AWACS:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
self.corridorfloor = Floor
self.corridorceiling = Ceiling
return self
end
--- [User] Draw a line around the FEZ on the F10 map.
-- @param #AWACS self
-- @return #AWACS self
@@ -4336,6 +4385,14 @@ function AWACS:_StartIntel(awacs)
intel:SetFilterCategory({Unit.Category.AIRPLANE,Unit.Category.HELICOPTER})
end
-- Corridors
if self.usecorridors == true then
intel:SetCorridorZones(self.corridorzones)
if self.corridorceiling or self.corridorfloor then
intel:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
-- Callbacks
local function NewCluster(Cluster)
self:__NewCluster(5,Cluster)
+83 -32
View File
@@ -31,7 +31,7 @@
-- @image OPS_CSAR.jpg
---
-- Last Update Oct 2025
-- Last Update Dec 2025
-------------------------------------------------------------------------
--- **CSAR** class, extends Core.Base#BASE, Core.Fsm#FSM
@@ -81,7 +81,7 @@
--
-- mycsar.allowDownedPilotCAcontrol = false -- Set to false if you don\'t want to allow control by Combined Arms.
-- mycsar.allowFARPRescue = true -- allows pilots to be rescued by landing at a FARP or Airbase. Else MASH only!
-- mycsar.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
-- mycsar.FARPRescueDistance = 500 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
-- mycsar.autosmoke = false -- automatically smoke a downed pilot\'s location when a heli is near.
-- mycsar.autosmokedistance = 1000 -- distance for autosmoke
-- mycsar.coordtype = 1 -- Use Lat/Long DDM (0), Lat/Long DMS (1), MGRS (2), Bullseye imperial (3) or Bullseye metric (4) for coordinates.
@@ -119,6 +119,7 @@
-- mycsar.PilotWeight = 80 -- Loaded pilots weigh 80kgs each
-- mycsar.AllowIRStrobe = false -- Allow a menu item to request an IR strobe to find a downed pilot at night (requires NVGs to see it).
-- mycsar.IRStrobeRuntime = 300 -- If an IR Strobe is activated, it runs for 300 seconds (5 mins).
-- mycsar.EnableMenuSmokeMASH = true -- Allow a menu item to request smoke at the closest MASH/AFB for rescue.
--
-- ## 2.1 Create own SET_GROUP to manage CTLD Pilot groups
--
@@ -272,6 +273,8 @@ CSAR = {
UserSetGroup = nil,
AllowIRStrobe = false,
IRStrobeRuntime = 300,
FARPRescueDistance = 500,
EnableMenuSmokeMASH = true,
}
--- Downed pilots info.
@@ -315,7 +318,7 @@ CSAR.AircraftType["CH-47Fbl1"] = 31
--- CSAR class version.
-- @field #string version
CSAR.version="1.0.34"
CSAR.version="1.0.35"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
@@ -431,12 +434,13 @@ function CSAR:New(Coalition, Template, Alias)
self.radioSound = "beacon.ogg" -- the name of the sound file to use for the Pilot radio beacons. If this isnt added to the mission BEACONS WONT WORK!
self.beaconRefresher = 29 -- seconds
self.allowFARPRescue = true --allows pilot to be rescued by landing at a FARP or Airbase
self.FARPRescueDistance = 1000 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
self.FARPRescueDistance = 500 -- you need to be this close to a FARP or Airport for the pilot to be rescued.
self.max_units = 6 --max number of pilots that can be carried
self.useprefix = true -- Use the Prefixed defined below, Requires Unit have the Prefix defined below
self.csarPrefix = { "helicargo", "MEDEVAC"} -- prefixes used for useprefix=true - DON\'T use # in names!
self.template = Template or "generic" -- template for downed pilot
self.mashprefix = {"MASH"} -- prefixes used to find MASHes
self.EnableMenuSmokeMASH = true
self.autosmoke = false -- automatically smoke location when heli is near
self.autosmokedistance = 2000 -- distance for autosmoke
@@ -673,7 +677,7 @@ function CSAR:_CreateDownedPilotTrack(Group,Groupname,Side,OriginalUnit,Descript
DownedPilot.alive = true
DownedPilot.wetfeet = Wetfeet or false
DownedPilot.BeaconName = BeaconName
-- Add Pilot
local PilotTable = self.downedPilots
local counter = self.downedpilotcounter
@@ -851,6 +855,10 @@ function CSAR:_AddCsar(_coalition , _country, _point, _typeName, _unitName, _pla
else
BeaconName = "Ghost-1-1"..math.random(1,10000)
end
if _playerName == nil or _playerName == "" then
_playerName = "AI MIA"
end
if (_freq and _freq ~= 0) then --shagrat only add beacon if _freq is NOT 0
self:_AddBeaconToGroup(_spawnedGroup, _freq, BeaconName)
@@ -1232,7 +1240,7 @@ function CSAR:_EventHandler(EventData)
local _place = _event.Place -- Wrapper.Airbase#AIRBASE
if _place == nil then
self:T(self.lid .. " Landing Place Nil")
self:T(self.lid .. " Landing Place nil")
return self -- error!
end
@@ -1267,7 +1275,7 @@ function CSAR:_EventHandler(EventData)
self:T("Country = ".._country.." Coalition = ".._coalition)
if _coalition == self.coalition then
local _freq = self:_GenerateADFFrequency()
self:I({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq})
self:T({coalition=_coalition,country= _country, coord=_LandingPos, name=_unitname, player=_event.IniPlayerName, freq=_freq})
self:_AddCsar(_coalition, _country, _LandingPos, nil, _unitname, _event.IniPlayerName, _freq, self.suppressmessages, "none")--shagrat add CSAR at Parachute location.
Unit.destroy(_event.initiator) -- shagrat remove static Pilot model
@@ -1761,7 +1769,14 @@ function CSAR:_ScheduledSARFlight(heliname,groupname, isairport, noreschedule, I
end
self:T(self.lid.."[Drop off debug] Check distance to MASH for "..heliname.." Distance km: "..math.floor(_dist/1000))
if self.verbose>0 then
local debugtext = string.format("Distance %dm | Rescuedist %dm | IsAirport %s | IsInAir %s | IsHeloBase %s\n",_dist,self.FARPRescueDistance,tostring(isairport),tostring(_heliUnit:InAir()),tostring(IsHeloBase))
self:T("*******************************")
self:T(debugtext)
self:T("*******************************")
end
if ( _dist < self.FARPRescueDistance or isairport ) and ((_heliUnit:InAir() == false) or (IsHeloBase == true)) then
self:T(self.lid.."[Drop off debug] Distance ok, door check")
if self.pilotmustopendoors and self:_IsLoadingDoorOpen(heliname) == false then
@@ -2138,15 +2153,52 @@ function CSAR:_Reqsmoke( _unitName )
return self
end
---(Internal) Request smoke at closest MASH/AFB.
--@param #CSAR self
--@param #string _unitName Name of the helicopter
function CSAR:_ReqsmokeMash( _unitName )
self:T(self.lid .. " _ReqsmokeMash")
local _heli = self:_GetSARHeli(_unitName)
if _heli == nil then
return
end
local smokedist = 8000
if smokedist < self.approachdist_far then smokedist = self.approachdist_far end
local distance, name, coordinate = self:_GetClosestMASH(_heli)
if coordinate and distance then
local disttext
if _SETTINGS:IsImperial() then
disttext = string.format("%.1fnm",UTILS.MetersToNM(distance))
else
disttext = string.format("%.1fkm",distance/1000)
end
local _msg = string.format("%s - Popping smoke at the closest rescue point: %s", self:_GetCustomCallSign(_unitName), disttext)
self:_DisplayMessageToSAR(_heli, _msg, self.messageTime, false, true, true)
local color = self.smokecolor
coordinate:Smoke(color)
else
local _distance = string.format("%.1fkm",smokedist/1000)
if _SETTINGS:IsImperial() then
_distance = string.format("%.1fnm",UTILS.MetersToNM(smokedist))
else
_distance = string.format("%.1fkm",smokedist/1000)
end
self:_DisplayMessageToSAR(_heli, string.format("No rescue point within %s",_distance), self.messageTime, false, false, true)
end
return self
end
--- (Internal) Determine distance to closest MASH.
-- @param #CSAR self
-- @param Wrapper.Unit#UNIT _heli Helicopter #UNIT
-- @return #number Distance in meters
-- @return #string MASH Name as string
-- @return Core.Point#COORDINATE Coordinate The MASH/AFB Coordinate (for smoke)
function CSAR:_GetClosestMASH(_heli)
self:T(self.lid .. " _GetClosestMASH")
local _mashset = self.mash -- Core.Set#SET_GROUP
local MashSets = {}
local MashSets = {}
--local _mashes = _mashset.Set-- #table
table.insert(MashSets,_mashset.Set)
table.insert(MashSets,self.zonemashes.Set)
@@ -2155,12 +2207,21 @@ function CSAR:_GetClosestMASH(_heli)
local _distance = 0
local _helicoord = _heli:GetCoordinate()
local MashName = nil
local Coordinate = nil -- Core.Point#COORDINATE
if self.allowFARPRescue then
local position = _heli:GetCoordinate()
local afb,distance = position:GetClosestAirbase(nil,self.coalition)
local afb,distance = _helicoord:GetClosestAirbase(nil,self.coalition)
_shortestDistance = distance
MashName = (afb ~= nil) and afb:GetName() or "Unknown"
Coordinate = (afb ~= nil) and afb:GetCoordinate()
if afb then
local afbzone = afb:GetMinimumBoundingCircleFromParkingSpots()
if afbzone then
if afbzone:IsCoordinateInZone(_helicoord) and distance > self.FARPRescueDistance*1.1 then
_shortestDistance = 100
end
end
end
end
for _,_mashes in pairs(MashSets) do
@@ -2175,12 +2236,13 @@ function CSAR:_GetClosestMASH(_heli)
if _distance ~= nil and (_shortestDistance == -1 or _distance < _shortestDistance) then
_shortestDistance = _distance
MashName = _mashUnit:GetName() or "Unknown"
Coordinate = _mashcoord
end
end
end
if _shortestDistance ~= -1 then
return _shortestDistance, MashName
return _shortestDistance, MashName, Coordinate
else
return -1
end
@@ -2249,6 +2311,9 @@ function CSAR:_AddMedevacMenuItem()
local _rootMenu4 = MENU_GROUP_COMMAND:New(_group,"Request Smoke",_rootPath, self._Reqsmoke,self,_unitName)
if self.AllowIRStrobe then
local _rootMenu5 = MENU_GROUP_COMMAND:New(_group,"Request IR Strobe",_rootPath, self._ReqIRStrobe,self,_unitName):Refresh()
end
if self.EnableMenuSmokeMASH then
local _rootMenu6 = MENU_GROUP_COMMAND:New(_group,"Smoke Closest MASH",_rootPath, self._ReqsmokeMash,self,_unitName)
else
_rootMenu4:Refresh()
end
@@ -2269,6 +2334,7 @@ function CSAR:_GetDistance(_point1, _point2)
if _point1 and _point2 then
local distance1 = _point1:Get2DDistance(_point2)
local distance2 = _point1:DistanceFromPointVec2(_point2)
MESSAGE:New(string.format("_GetDistance: d1 = %dm | d2 = %dm",distance1,distance2)):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1)
if distance1 and type(distance1) == "number" then
return distance1
elseif distance2 and type(distance2) == "number" then
@@ -2368,7 +2434,7 @@ function CSAR:_AddBeaconToGroup(_group, _freq, BeaconName)
--local name = _radioUnit:GetName()
local Sound = "l10n/DEFAULT/"..self.radioSound
local vec3 = _radioUnit:GetVec3() or _radioUnit:GetPositionVec3() or {x=0,y=0,z=0}
self:I(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z))
self:T(self.lid..string.format("Added Radio Beacon %d Hertz | Name %s | Position {%d,%d,%d}",Frequency,BeaconName,vec3.x,vec3.y,vec3.z))
trigger.action.radioTransmission(Sound, vec3, 0, true, Frequency, self.ADFRadioPwr or 500,BeaconName) -- Beacon in MP only runs for exactly 30secs straight
end
end
@@ -2496,22 +2562,6 @@ function CSAR:onafterStart(From, Event, To)
self.staticmashes = SET_STATIC:New():FilterCoalitions(self.coalitiontxt):FilterPrefixes(self.mashprefix):FilterStart()
self.zonemashes = SET_ZONE:New():FilterPrefixes(self.mashprefix):FilterStart()
--[[
if staticmashes:Count() > 0 then
for _,_mash in pairs(staticmashes.Set) do
self.mash:AddObject(_mash)
end
end
if zonemashes:Count() > 0 then
self:T("Adding zones to self.mash SET")
for _,_mash in pairs(zonemashes.Set) do
self.mash:AddObject(_mash)
end
self:T("Objects in SET: "..self.mash:Count())
end
--]]
if not self.coordinate then
local csarhq = self.mash:GetRandom()
if csarhq then
@@ -2854,6 +2904,7 @@ function CSAR:onafterSave(From, Event, To, path, filename)
if DownedPilot and DownedPilot.alive then
-- get downed pilot data for saving
local playerName = DownedPilot.player
if playerName == nil or playerName == "" then playerName = "AI MIA" end
local group = DownedPilot.group
local coalition = group:GetCoalition()
local country = group:GetCountry()
@@ -2864,7 +2915,7 @@ function CSAR:onafterSave(From, Event, To, path, filename)
local unitName = DownedPilot.originalUnit
local txt = string.format("%s,%d,%d,%d,%s,%s,%s,%s,%s,%d\n",playerName,location.x,location.y,location.z,coalition,country,description,typeName,unitName,freq)
self:I(self.lid.."Saving to CSAR File: " .. txt)
self:T(self.lid.."Saving to CSAR File: " .. txt)
data = data .. txt
end
@@ -2979,8 +3030,8 @@ function CSAR:onafterLoad(From, Event, To, path, filename)
-- Info message.
local text=string.format("Loading CSAR state from file %s", filename)
MESSAGE:New(text,10):ToAllIf(self.Debug)
self:I(self.lid..text)
MESSAGE:New(text,10):ToAllIf(self.verbose>0)
self:T(self.lid..text)
local file=assert(io.open(filename, "rb"))
File diff suppressed because it is too large Load Diff
+948
View File
@@ -0,0 +1,948 @@
--- **Ops** - Create your A2G Defenses.
--
-- **Main Features:**
--
-- * Automatically create and manage A2G defenses using an AirWing and Squadrons for one coalition
-- * Easy set-up
--
-- ===
--
-------------------------------------------------------------------------
-- Easy A2G Engagement Class, based on OPS classes
-------------------------------------------------------------------------
--
-- ## Documentation:
--
-- https://flightcontrol-master.github.io/MOOSE_DOCS_DEVELOP/Documentation/Ops.EasyAG.html
--
-- ## Example Missions:
--
-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/Ops/EasyAG).
--
-------------------------------------------------------------------------
-- Date: Dec 2025
-- Last Update: Dec 2025
-------------------------------------------------------------------------
--
-- ===
--
-- **Main Features:**
--
-- * Automatically create and manage A2G defenses using an AirWing and Squadrons for one coalition
-- * Easy set-up
-- * Add additional AirWings on other airbases
-- * Each wing can have more than one Squadron - tasking to Squadrons is done on a random basis per AirWing
-- * Create borders and zones of engagement
-- * Detection can be ground based and/or via AWACS
--
-- ===
--
-- ### AUTHOR: **applevangelist**
--
-- @module Ops.EasyAG
-- @image AI_Air_To_Ground_Dispatching.JPG
--- EASYA2G Class
-- @type EASYA2G
-- @field #string ClassName
-- @field #number overhead
-- @field #number engagerange
-- @field #number capgrouping
-- @field #string airbasename
-- @field Wrapper.Airbase#AIRBASE airbase
-- @field #number coalition
-- @field #string alias
-- @field #table wings
-- @field Ops.Intel#INTEL Intel
-- @field #number resurrection
-- @field #number capspeed
-- @field #number capalt
-- @field #number capdir
-- @field #number capleg
-- @field #number maxinterceptsize
-- @field #number missionrange
-- @field #number noalert5
-- @field #table ManagedAW
-- @field #table ManagedSQ
-- @field #table ManagedCP
-- @field #table ManagedTK
-- @field #table ManagedEWR
-- @field #table ManagedREC
-- @field #number MaxAliveMissions
-- @field #boolean debug
-- @field #number repeatsonfailure
-- @field Core.Set#SET_ZONE GoZoneSet
-- @field Core.Set#SET_ZONE NoGoZoneSet
-- @field Core.Set#SET_ZONE ConflictZoneSet
-- @field #boolean Monitor
-- @field #boolean TankerInvisible
-- @field #number CapFormation
-- @field #table ReadyFlightGroups
-- @field #boolean DespawnAfterLanding
-- @field #boolean DespawnAfterHolding
-- @field #list<Ops.Auftrag#AUFTRAG> ListOfAuftrag
-- @field #string defaulttakeofftype Take off type
-- @field #number FuelLowThreshold
-- @field #number FuelCriticalThreshold
-- @field #boolean showpatrolpointmarks
-- @field #table EngageTargetTypes
-- @extends Ops.EasyGCICAP#EASYGCICAP
--- *“High-Threat Close-Air-Support is a Myth.”* -- Mike “Starbaby” Pietrucha.
--
-- ===
--
-- # The EasyAG Concept
--
-- The idea of this class is partially to make the OPS classes easier operational for an A2G defense network, and to replace the legacy AI_A2G_Dispatcher system - not to it's
-- full extent, but make a basic system work very quickly.
--
-- # Setup
--
-- ## Basic understanding
--
-- The basics are, there is **one** and only **one** AirWing per airbase. Each AirWing has **at least** one Squadron, who will do A2G tasks. Squadrons will be randomly chosen for the task at hand.
-- Each AirWing has **at least** one Conflict Zone that it manages. COnflict Zones will be covered by the AirWing automatically as long as airframes are available. Detected enemy ground forces will be assigned to **one**
-- AirWing based on proximity (that is, if you have more than one).
--
-- ## Assignment of tasks for enemies
--
-- An exisiting plane or a newly spawned plane will take care of the intruders. Standard overhead is 0.1, i.e. a group of 10 intrudes will
-- be managed by one planes from the assigned AirWing. There is an maximum missions limitation per AirWing, so we do not spam the skies.
--
-- ## Basic set-up code
--
-- ### Prerequisites
--
-- You have to put a **STATIC WAREHOUSE** object on the airbase with the UNIT name according to the name of the airbase. **Do not put any other static type or it creates a conflict with the airbase name!**
-- E.g. for Kutaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- Next put a late activated template group for your A2G Squadron on the map. Last, put a zone on the map for the Defense operations, let's name it "Blue Zone 1". Size of the zone plays no role.
-- Put a scout system on the map and name it aptly, like "Blue SCOUT".
--
-- ### Zones
--
-- For our example, you create a RED and a BLUE border, as a closed polygonal zone representing the borderlines. You can also have conflict zone, where - for our example - BLUE will attack
-- RED groups, despite being on or close to RED territory. Think of a no-fly zone or an limited area of engagement. Conflict zones take precedence over borders, i.e. they can overlap all borders.
--
-- ### Code it
--
-- -- Set up a basic system for the blue side, we'll reside on Kutaisi, and use GROUP objects with "Blue SCOUT" in the name as Detecting Systems.
-- local mywing = EASYA2G:New("A2G",AIRBASE.Caucasus.Kutaisi,"blue","SCOUT")
--
-- -- Add a holding/ingress point belonging to our airbase, we'll be at 5k ft doing 250 kn, initial direction 225 degrees (West), leg 5NM
-- -- This will effectively be the ingress coordinate into the cnflict zone
-- local Coordinate = ZONE:New("A2G Loitering"):GetCoordinate()
-- mywing:AddHoldingPointA2G(AIRBASE.Caucasus.Kutaisi,Coordinate,5000,250,225,5)
--
-- -- Add a recon point over the conflict zone, we'll use a reaper for recon
-- local Coordinate2 = ZONE:New("A2G Recon"):GetCoordinate()
-- mywing:AddPatrolPointRecon(AIRBASE.Caucasus.Kutaisi,Coordinate2,15000,225,225,5)
--
-- -- Add three Squadrons with templates "Hero 1" and "Hero 2", 20 airframes, skill as set
-- mywing:AddSquadron("A2G Flight", "Hero 1", AIRBASE.Caucasus.Kutaisi, 5, AI.Skill.GOOD, Modex, Livery)
-- mywing:AddSquadron("A2G Helo", "Hero 2", AIRBASE.Caucasus.Kutaisi, 5, AI.Skill.HIGH, Modex, Livery)
-- mywing:AddReconSquadron("Recon Drone", "SpyInTheSky SCOUT", AIRBASE.Caucasus.Kutaisi, 5, AI.Skill.EXCELLENT, Modex, Livery)
--
-- -- Ensure our reaper doesn't get immediately killed
-- mywing:SetTankerAndScoutsInvisible(true)
--
-- -- Add a couple of zones
-- -- We'll defend our own border
-- mywing:AddAcceptZone(ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) ))
-- -- We'll attack intruders also here - conflictzones can overlap borders(!) - limited zone of engagement
-- mywing:AddConflictZone(ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" )))
-- -- We'll leave the reds alone on their turf
-- mywing:AddRejectZone(ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) ))
--
-- -- Optional - Draw the borders on the map so we see what's going on
-- -- Set up borders on map
-- local BlueBorder = ZONE_POLYGON:New( "Blue Border", GROUP:FindByName( "Blue Border" ) )
-- BlueBorder:DrawZone(-1,{0,0,1},1,FillColor,FillAlpha,1,true)
-- local ConflictZone = ZONE_POLYGON:New("Red Defense Zone", GROUP:FindByName( "Red Defense Zone" ))
-- ConflictZone:DrawZone(-1,{1,1,0},1,FillColor,FillAlpha,2,true)
-- local BlueNoGoZone = ZONE_POLYGON:New( "Red Border", GROUP:FindByName( "Red Border" ) )
-- BlueNoGoZone:DrawZone(-1,{1,0,0},1,FillColor,FillAlpha,4,true)
--
-- ### Add a second airwing with squads and own patrol point (optional)
--
-- -- Set this up at Sukhumi
-- mywing:AddAirwing(AIRBASE.Caucasus.Sukhumi_Babushara,"Blue A2G Sukhumi")
-- -- A2G Point "Blue Zone 2"
-- mywing:AddPatrolPointA2G(AIRBASE.Caucasus.Sukhumi_Babushara,ZONE:FindByName("Blue Zone 2"):GetCoordinate(),30000,400,90,20)
--
-- -- This one has two squadrons to choose from
-- mywing:AddSquadron("Blue Sq3 F16","A2G Sukhumi II",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,402,"JASDF 6th TFS 43-8526 Skull Riders")
-- mywing:AddSquadron("Blue Sq2 F15","A2G Sukhumi I",AIRBASE.Caucasus.Sukhumi_Babushara,20,AI.Skill.GOOD,202,"390th Fighter SQN")
--
-- ### Add a tanker (optional)
--
-- -- **Note** If you need different tanker types, i.e. Boom and Drogue, set them up at different AirWings!
-- -- Add a tanker point
-- mywing:AddPatrolPointTanker(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone Tanker"):GetCoordinate(),20000,280,270,50)
-- -- Add a tanker squad - Radio 251 AM, TACAN 51Y
-- mywing:AddTankerSquadron("Blue Tanker","Tanker Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.EXCELLENT,602,nil,251,radio.modulation.AM,51)
--
-- ### Add an AWACS (optional)
--
-- -- Add an AWACS point
-- mywing:AddPatrolPointAwacs(AIRBASE.Caucasus.Kutaisi,ZONE:FindByName("Blue Zone AWACS"):GetCoordinate(),25000,300,270,50)
-- -- Add an AWACS squad - Radio 251 AM, TACAN 51Y
-- mywing:AddAWACSSquadron("Blue AWACS","AWACS Ops Kutaisi",AIRBASE.Caucasus.Kutaisi,20,AI.Skill.AVERAGE,702,nil,271,radio.modulation.AM)
--
-- # Fine-Tuning
--
-- ## Change Defaults
--
-- * @{#EASYA2G.SetDefaultResurrection}: Set how many seconds the AirWing stays inoperable after the AirWing STATIC HQ ist destroyed, default 900 secs.
-- * @{#EASYA2G.SetDefaultA2GSpeed}: Set how many knots the A2G flights should do (will be altitude corrected), default 225 kn.
-- * @{#EASYA2G.SetDefaultA2GAlt}: Set at which altitude (ASL) the A2G planes will fly, default 10,000 ft.
-- * @{#EASYA2G.SetDefaultA2GDirection}: Set the initial direction from the A2G point the planes will fly in degrees, default is 90°.
-- * @{#EASYA2G.SetDefaultA2GLeg}: Set the length of the A2G leg, default is 5 NM.
-- * @{#EASYA2G.SetDefaultA2GGrouping}: Set how many planes will be spawned per mission (CVAP/GCI), defaults to 1.
-- * @{#EASYA2G.SetDefaultMissionRange}: Set how many NM the planes can go from the home base, defaults to 100.
-- * @{#EASYA2G.SetDefaultNumberAlert5Standby}: Set how many planes will be spawned on cold standby (Alert5), default 2.
-- * @{#EASYA2G.SetDefaultEngageRange}: Set max engage range for A2G flights if they detect intruders, defaults to 50.
-- * @{#EASYA2G.SetMaxAliveMissions}: Set max parallel missions can be done (A2G+GCI+Alert5+Tanker+AWACS), defaults to 8.
-- * @{#EASYA2G.SetDefaultRepeatOnFailure}: Set max repeats on failure for intercepting/killing intruders, defaults to 3.
-- * @{#EASYA2G.SetTankerAndScoutsInvisible}: Set Tanker and Scouts to be invisible to enemy AI eyes. Is set to `true` by default.
--
-- ## Debug and Monitor
--
-- mywing.debug = true -- log information
-- mywing.Monitor = true -- show some statistics on screen
--
--
-- @field #EASYA2G
EASYA2G = {
ClassName = "EASYA2G",
overhead = 0.2,
capgrouping = 1,
airbasename = nil,
airbase = nil,
coalition = "blue",
alias = nil,
wings = {},
Intel = nil,
resurrection = 900,
capspeed = 300,
capalt = 25000,
capdir = 45,
capleg = 5,
maxinterceptsize = 2,
missionrange = 100,
noalert5 = 2,
ManagedAW = {},
ManagedSQ = {},
ManagedCP = {},
ManagedTK = {},
ManagedEWR = {},
ManagedREC = {},
MaxAliveMissions = 8,
debug = false,
engagerange = 50,
repeatsonfailure = 3,
GoZoneSet = nil,
NoGoZoneSet = nil,
ConflictZoneSet = nil,
Monitor = false,
TankerInvisible = true,
CapFormation = nil,
ReadyFlightGroups = {},
DespawnAfterLanding = false,
DespawnAfterHolding = true,
ListOfAuftrag = {},
defaulttakeofftype = "hot",
FuelLowThreshold = 25,
FuelCriticalThreshold = 10,
showpatrolpointmarks = false,
EngageTargetTypes = {"Ground"},
}
--- Internal Squadron data type
-- @type EASYA2G.Squad
-- @field #string TemplateName
-- @field #string SquadName
-- @field #string AirbaseName
-- @field #number AirFrames
-- @field #string Skill
-- @field #string Modex
-- @field #string Livery
-- @field #boolean Tanker
-- @field #boolean AWACS
-- @field #boolean RECON
-- @field #number Frequency
-- @field #number Modulation
-- @field #number TACAN
--- Internal Wing data type
-- @type EASYA2G.Wing
-- @field #string AirbaseName
-- @field #string Alias
-- @field #string CapZoneName
--- Internal CapPoint data type
-- @type EASYA2G.CapPoint
-- @field #string AirbaseName
-- @field Core.Point#COORDINATE Coordinate
-- @field #number Altitude
-- @field #number Speed
-- @field #number Heading
-- @field #number LegLength
-- @field Core.Zone#ZONE_BASE Zone
--- EASYA2G class version.
-- @field #string version
EASYA2G.version="0.1.3"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: TBD
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new GCIA2G Manager
-- @param #EASYA2G self
-- @param #string Alias A Name for this A2G Setup
-- @param #string AirbaseName Name of the Home Airbase
-- @param #string Coalition Coalition, e.g. "blue" or "red"
-- @param #string ScoutName (Partial) group name of the detection system of the coalition, e.g. "Red SCOUT", can be handed in as table of names, e.g.{"SCOUT","DRONE","SAM"}
-- @return #EASYA2G self
function EASYA2G:New(Alias, AirbaseName, Coalition, ScoutName)
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, EASYGCICAP:New(Alias, AirbaseName, Coalition, ScoutName)) -- #EASYA2G
-- defaults
self.alias = Alias or AirbaseName.." A2G Wing"
self.coalitionname = string.lower(Coalition) or "blue"
self.coalition = self.coalitionname == "blue" and coalition.side.BLUE or coalition.side.RED
self.wings = {}
if type(ScoutName) == "string" then ScoutName = {ScoutName} end
self.EWRName = ScoutName --or self.coalitionname.." EWR"
--self.CapZoneName = CapZoneName
self.airbasename = AirbaseName
self.airbase = AIRBASE:FindByName(self.airbasename)
self.GoZoneSet = SET_ZONE:New()
self.NoGoZoneSet = SET_ZONE:New()
self.ConflictZoneSet = SET_ZONE:New()
self.resurrection = 900
self.capspeed = 225
self.capalt = 5000
self.capdir = 90
self.capleg = 5
self.capgrouping = 2
self.missionrange = 100
self.noalert5 = 2
self.MaxAliveMissions = 8
self.engagerange = 50
self.repeatsonfailure = 3
self.Monitor = false
self.TankerInvisible = true
self.CapFormation = ENUMS.Formation.FixedWing.FingerFour.Group
self.DespawnAfterLanding = false
self.DespawnAfterHolding = true
self.ListOfAuftrag = {}
self.defaulttakeofftype = "hot"
self.FuelLowThreshold = 25
self.FuelCriticalThreshold = 10
self.showpatrolpointmarks = false
self.EngageTargetTypes = {"Ground"}
-- Set some string id for output to DCS.log file.
self.lid=string.format("EASYA2G %s | ", self.alias)
-- Add FSM transitions.
-- From State --> Event --> To State
self:SetStartState("Stopped")
self:AddTransition("Stopped", "Start", "Running")
self:AddTransition("Running", "Stop", "Stopped")
self:AddTransition("*", "Status", "*")
--- On Before "Start" event.
-- @function [parent=#EASYA2G] OnBeforeStart
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Start" event.
-- @function [parent=#EASYA2G] OnAfterStart
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On Before "Status" event.
-- @function [parent=#EASYA2G] OnBeforeStatus
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Status" event.
-- @function [parent=#EASYA2G] OnAfterStatus
-- @param #EASYA2G self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
self:AddAirwing(self.airbasename,self.alias,self.CapZoneName)
self:I(self.lid.."Created new instance (v"..self.version..")")
self:__Start(math.random(6,12))
return self
end
-------------------------------------------------------------------------
-- Functions
-------------------------------------------------------------------------
--- Set Tanker and Scouts to be invisible to enemy AI eyes
-- @param #EASYA2G self
-- @param #boolean Switch Set to true or false, by default this is set to true already
-- @return #EASYA2G self
function EASYA2G:SetTankerAndScoutsInvisible(Switch)
self:T(self.lid.."SetTankerAndScoutsInvisible")
self.TankerInvisible = Switch
return self
end
--- Set default A2G Speed in knots
-- @param #EASYA2G self
-- @param #number Speed Speed defaults to 300
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GSpeed(Speed)
self:T(self.lid.."SetDefaultSpeed")
self.capspeed = Speed or 300
return self
end
--- Set A2G Flight formation.
-- @param #EASYA2G self
-- @param #number Formation Formation to fly, defaults to ENUMS.Formation.FixedWing.FingerFour.Group
-- @return #EASYA2G self
function EASYA2G:SetA2GFormation(Formation)
self:T(self.lid.."SetA2GFormation")
self.CapFormation = Formation
return self
end
--- Set default A2G Altitude in feet
-- @param #EASYA2G self
-- @param #number Altitude Altitude defaults to 25000
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GAlt(Altitude)
self:T(self.lid.."SetDefaultAltitude")
self.capalt = Altitude or 25000
return self
end
--- Set default A2G lieg initial direction in degrees
-- @param #EASYA2G self
-- @param #number Direction Direction defaults to 90 (East)
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GDirection(Direction)
self:T(self.lid.."SetDefaultDirection")
self.capdir = Direction or 90
return self
end
--- Set default leg length in NM
-- @param #EASYA2G self
-- @param #number Leg Leg defaults to 5
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GLeg(Leg)
self:T(self.lid.."SetDefaultLeg")
self.capleg = Leg or 5
return self
end
--- Set default grouping, i.e. how many airplanes per A2G point
-- @param #EASYA2G self
-- @param #number Grouping Grouping defaults to 2
-- @return #EASYA2G self
function EASYA2G:SetDefaultA2GGrouping(Grouping)
self:T(self.lid.."SetDefaultA2GGrouping")
self.capgrouping = Grouping or 2
return self
end
--- Set A2G mission start to vary randomly between Start end End seconds.
-- @param #EASYA2G self
-- @param #number Start
-- @param #number End
-- @return #EASYA2G self
function EASYA2G:SetA2GStartTimeVariation(Start, End)
self.capOptionVaryStartTime = Start or 5
self.capOptionVaryEndTime = End or 60
return self
end
--- Set which target types A2G flights will prefer to engage, defaults to {"Ground"}
-- @param #EASYA2G self
-- @param #table types Table of comma separated #string entries, defaults to {"Ground"} (everything that is ground and is not a weapon). Useful other options are e.g. {"Armored vehicles"}, {"Tanks"},
-- or {"APC"} or combinations like {"APC", "Tanks", "Artillery"}. See [Hoggit Wiki](https://wiki.hoggitworld.com/view/DCS_enum_attributes).
-- @return #EASYA2G self
function EASYA2G:SetA2GEngageTargetTypes(types)
self.EngageTargetTypes = types or {"Ground"}
return self
end
--- Add a A2G patrol/holding point to a Wing
-- @param #EASYA2G self
-- @param #string AirbaseName Name of the Wing's airbase
-- @param Core.Point#COORDINATE Coordinate. Can be handed as a Core.Zone#ZONE object (e.g. in case you want the point to align with a moving zone).
-- @param #number Altitude Defaults to 25000 feet ASL.
-- @param #number Speed Defaults to 300 knots TAS.
-- @param #number Heading Defaults to 90 degrees (East).
-- @param #number LegLength Defaults to 15 NM.
-- @return #EASYA2G self
function EASYA2G:AddHoldingPointA2G(AirbaseName,Coordinate,Altitude,Speed,Heading,LegLength)
self:T(self.lid.."AddHoldingPointA2G")--..Coordinate:ToStringLLDDM())
local coordinate = Coordinate
local EntryCAP = {} -- #EASYGCICAP.CapPoint
if Coordinate:IsInstanceOf("ZONE_BASE") then
-- adjust coordinate and get the coordinate from the zone
coordinate = Coordinate:GetCoordinate()
EntryCAP.Zone = Coordinate
end
EntryCAP.AirbaseName = AirbaseName
EntryCAP.Coordinate = coordinate
EntryCAP.Altitude = Altitude or 25000
EntryCAP.Speed = Speed or 300
EntryCAP.Heading = Heading or 90
EntryCAP.LegLength = LegLength or 5
self.ManagedCP[#self.ManagedCP+1] = EntryCAP
if self.debug then
local mark = MARKER:New(coordinate,self.lid.."Holding Point"):ToAll()
end
return self
end
--- (Internal) Add a Squadron to an Airwing of the manager
-- @param #EASYA2G self
-- @param #string TemplateName Name of the group template.
-- @param #string SquadName Squadron name - must be unique!
-- @param #string AirbaseName Name of the airbase the airwing resides on, e.g. AIRBASE.Caucasus.Kutaisi
-- @param #number AirFrames Number of available airframes, e.g. 20.
-- @param #string Skill(optional) Skill level, e.g. AI.Skill.AVERAGE
-- @param #string Modex (optional) Modex to be used,e.g. 402.
-- @param #string Livery (optional) Livery name to be used.
-- @param #number Frequency (optional) Radio Frequency to be used.
-- @param #number Modulation (optional) Radio Modulation to be used, e.g. radio.modulation.AM or radio.modulation.FM
-- @return #EASYA2G self
function EASYA2G:_AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames, Skill, Modex, Livery, Frequency, Modulation)
self:T(self.lid.."_AddSquadron "..SquadName)
-- Add Squadrons
local Squadron_One = SQUADRON:New(TemplateName,AirFrames,SquadName)
Squadron_One:AddMissionCapability({AUFTRAG.Type.CAS, AUFTRAG.Type.CASENHANCED, AUFTRAG.Type.BAI, AUFTRAG.Type.ALERT5, AUFTRAG.Type.BOMBING, AUFTRAG.Type.STRIKE})
--Squadron_One:SetFuelLowRefuel(true)
Squadron_One:SetFuelLowThreshold(0.3)
Squadron_One:SetTurnoverTime(10,20)
Squadron_One:SetModex(Modex)
Squadron_One:SetLivery(Livery)
Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
Squadron_One:SetMissionRange(self.missionrange)
local wing = self.wings[AirbaseName][1] -- Ops.Airwing#AIRWING
wing:AddSquadron(Squadron_One)
wing:NewPayload(TemplateName,-1,{AUFTRAG.Type.CAS, AUFTRAG.Type.CASENHANCED, AUFTRAG.Type.BAI, AUFTRAG.Type.ALERT5, AUFTRAG.Type.BOMBING, AUFTRAG.Type.STRIKE},75)
return self
end
--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready.
-- @param #EASYA2G self
-- @param #table ReadyFlightGroups ReadyFlightGroups
-- @param Ops.Auftrag#AUFTRAG Auftrag The Auftrag
-- @param Wrapper.Group#GROUP Group The Target
-- @param #number WingSize Calculated number of Flights
-- @return #boolean assigned
-- @return #number leftover
function EASYA2G:_TryAssignMission(ReadyFlightGroups,Auftrag,Group,WingSize)
self:T("_TryAssignMission for size "..WingSize or 1)
local assigned = false
local wingsize = WingSize or 1
local mindist = 0
local disttable = {}
if Group and Group:IsAlive() then
local gcoord = Group:GetCoordinate() or COORDINATE:New(0,0,0)
self:T(self.lid..string.format("Assignment for %s",Group:GetName()))
for _name,_FG in pairs(ReadyFlightGroups or {}) do
local FG = _FG -- Ops.FlightGroup#FLIGHTGROUP
local fcoord = FG:GetCoordinate()
local dist = math.floor(UTILS.Round(fcoord:Get2DDistance(gcoord)/1000,1))
self:T(self.lid..string.format("FG %s Distance %dkm",_name,dist))
disttable[#disttable+1] = { FG=FG, dist=dist}
if dist>mindist then mindist=dist end
end
local function sortDistance(a, b)
return a.dist < b.dist
end
table.sort(disttable, sortDistance)
for _,_entry in ipairs(disttable) do
local FG = _entry.FG -- Ops.FlightGroup#FLIGHTGROUP
FG:AddMission(Auftrag)
local cm = FG:GetMissionCurrent()
if cm then cm:Cancel() end
wingsize = wingsize - 1
self:T(self.lid..string.format("Assigned to FG %s Distance %dkm",FG:GetName(),_entry.dist))
if wingsize == 0 then
assigned = true
break
end
end
end
return assigned, wingsize
end
--- Find a holding point closest to the group to be attacked (if any set)
-- @param #EASYA2G self
-- @param Wrapper.Group#GROUP Group
-- @return Core.Point#COORDINATE Point (can be nil!)
function EASYA2G:_GetClosestHoldingPoint(Group)
local point = nil
local mindist = 0
if Group and Group:IsAlive() then
local gcoord = Group:GetCoordinate() or COORDINATE:New(0,0,0)
for _,_data in pairs(self.ManagedCP or {}) do
local data = _data -- #EASYGCICAP.CapPoint
--data.Coordinate
local dist = math.floor(UTILS.Round(data.Coordinate:Get2DDistance(gcoord)/1000,1))
self:T(self.lid..string.format("Holding Point Distance %dkm",dist))
if dist>mindist then
mindist=dist
point=data.Coordinate
end
end
end
return point
end
--- Here, we'll decide if we need to launch an attacking flight, and from where
-- @param #EASYA2G self
-- @param Ops.Intel#INTEL.Cluster Cluster
-- @return #EASYA2G self
function EASYA2G:_AssignMission(Cluster)
self:I(self.lid.."_AssignMission")
-- Here, we'll decide if we need to launch an attacking flight, and from where
local overhead = self.overhead
local capspeed = self.capspeed + 100
local capalt = self.capalt or 5000
local maxsize = self.maxinterceptsize
local repeatsonfailure = self.repeatsonfailure
local wings = self.wings
local ctlpts = self.ManagedCP
local MaxAliveMissions = self.MaxAliveMissions --* self.capgrouping
local nogozoneset = self.NoGoZoneSet
local conflictzoneset = self.ConflictZoneSet
local ReadyFlightGroups = self.ReadyFlightGroups
-- Aircraft?
if Cluster.ctype == INTEL.Ctype.AIRCRAFT then return end
-- Threatlevel 0..10
local contact = self.Intel:GetHighestThreatContact(Cluster)
local name = contact.groupname --#string
local threat = contact.threatlevel --#number
local position = self.Intel:CalcClusterFuturePosition(Cluster,300)
-- calculate closest zone
local bestdistance = 2000*1000 -- 2000km
local targetairwing = nil -- Ops.Airwing#AIRWING
local targetawname = "" -- #string
local clustersize = self.Intel:ClusterCountUnits(Cluster) or 1
local wingsize = math.abs(overhead * (clustersize+1))
if wingsize > maxsize then wingsize = maxsize end
-- existing mission, and if so - done?
local retrymission = true
if Cluster.mission and (not Cluster.mission:IsOver()) then
retrymission = false
end
if (retrymission) and (wingsize >= 1) then
MESSAGE:New(string.format("**** %s Attackers need wingsize %d", UTILS.GetCoalitionName(self.coalition), wingsize),15,"A2G"):ToAllIf(self.debug):ToLog()
for _,_data in pairs (wings) do
local airwing = _data[1] -- Ops.Airwing#AIRWING
local zone = _data[2] -- Core.Zone#ZONE
local zonecoord = zone:GetCoordinate()
local name = _data[3] -- #string
local coa = AIRBASE:FindByName(name):GetCoalition()
local distance = position:DistanceFromPointVec2(zonecoord)
local airframes = airwing:CountAssets(true)
local samecoalitionab = coa == self.coalition and true or false
if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then
bestdistance = distance
targetairwing = airwing
targetawname = name
end
end
for _,_data in pairs (ctlpts) do
--local airwing = _data[1] -- Ops.Airwing#AIRWING
--local zone = _data[2] -- Core.Zone#ZONE
--local zonecoord = zone:GetCoordinate()
--local name = _data[3] -- #string
local data = _data -- #EASYGCICAP.CapPoint
local name = data.AirbaseName
local zonecoord = data.Coordinate
if data.Zone then
-- refresh coordinate in case we have a (moving) zone
zonecoord = data.Zone:GetCoordinate()
end
local airwing = wings[name][1]
local coa = AIRBASE:FindByName(name):GetCoalition()
local samecoalitionab = coa == self.coalition and true or false
local distance = position:DistanceFromPointVec2(zonecoord)
local airframes = airwing:CountAssets(true)
if distance < bestdistance and airframes >= wingsize and samecoalitionab == true then
bestdistance = distance
targetairwing = airwing -- Ops.Airwing#AIRWING
targetawname = name
end
end
local text = string.format("Closest Airwing is %s", targetawname)
local m = MESSAGE:New(text,10,"EasyA2G"):ToAllIf(self.debug):ToLog()
-- Do we have a matching airwing?
if targetairwing then
local AssetCount = targetairwing:CountAssetsOnMission(MissionTypes,Cohort)
local missioncount = self:_CountAliveAuftrags()
-- Enough airframes on mission already?
self:T(self.lid.." Assets on Mission "..AssetCount)
if missioncount < MaxAliveMissions then
local repeats = repeatsonfailure
local Vec1 = contact.group:GetVec2()
local Vec2 = targetairwing:GetVec2()
--local HoldingVec2 = UTILS.FindNearestPointOnCircle(Vec1,UTILS.NMToMeters(10),Vec2)
local IngressCoordinate = self:_GetClosestHoldingPoint(contact.group)
if IngressCoordinate == nil then
local IngressVec2 = UTILS.FindNearestPointOnCircle(Vec1,UTILS.NMToMeters(10),Vec2)
IngressCoordinate = COORDINATE:NewFromVec2(IngressVec2)
end
local InterceptAuftrag = AUFTRAG:NewBAI(contact.group,capalt)
:SetMissionRange(150)
:SetPriority(1,true,1)
:SetRepeatDelay(300)
--:SetRequiredAssets(wingsize)
:SetRepeatOnFailure(repeats)
:SetMissionSpeed(UTILS.KnotsToAltKIAS(capspeed,capalt))
:SetMissionAltitude(capalt)
-- TODO: Refine this
--:SetMissionHoldingCoord(COORDINATE:NewFromVec2(HoldingVec2),capalt,capspeed,120)
:SetMissionIngressCoord(IngressCoordinate,capalt,capspeed)
--:SetMissionEgressCoord(COORDINATE:NewFromVec2(HoldingVec2),capalt,capspeed)
if nogozoneset:Count() > 0 then
InterceptAuftrag:AddConditionSuccess(
function(group,zoneset,conflictset)
local success = false
if group and group:IsAlive() then
local coord = group:GetCoordinate()
if coord and zoneset:Count() > 0 and zoneset:IsCoordinateInZone(coord) then
success = true
end
if coord and conflictset:Count() > 0 and conflictset:IsCoordinateInZone(coord) then
success = false
end
else
success = true -- target dead
end
return success
end,
contact.group,
nogozoneset,
conflictzoneset
)
end
InterceptAuftrag:AddConditionFailure(
function()
local failure = false
if InterceptAuftrag:CountOpsGroups()==0 and InterceptAuftrag:IsExecuting() then failure = true end
return failure
end
)
table.insert(self.ListOfAuftrag,InterceptAuftrag)
local assigned, rest = self:_TryAssignMission(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize)
if not assigned then
InterceptAuftrag:SetRequiredAssets(rest)
targetairwing:AddMission(InterceptAuftrag)
end
Cluster.mission = InterceptAuftrag
end
else
MESSAGE:New("**** Not enough airframes available or max mission limit reached!",15,"EasyA2G"):ToAllIf(self.debug):ToLog()
end
end
end
--- (Internal) Start detection.
-- @param #EASYA2G self
-- @return #EASYA2G self
function EASYA2G:_StartIntel()
self:T(self.lid.."_StartIntel")
-- Border GCI Detection
local BlueAir_DetectionSetGroup = SET_GROUP:New()
BlueAir_DetectionSetGroup:FilterPrefixes( self.EWRName )
BlueAir_DetectionSetGroup:FilterStart()
-- Intel type detection
local BlueIntel = INTEL:New(BlueAir_DetectionSetGroup,self.coalitionname, self.alias)
BlueIntel:SetClusterAnalysis(true,false,false)
BlueIntel:SetForgetTime(300)
BlueIntel:SetAcceptZones(self.GoZoneSet)
BlueIntel:SetRejectZones(self.NoGoZoneSet)
BlueIntel:SetConflictZones(self.ConflictZoneSet)
BlueIntel:SetVerbosity(0)
if self.usecorridors == true then
BlueIntel:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling)
end
end
BlueIntel:Start()
if self.debug then
BlueIntel.debug = true
end
local function AssignCluster(Cluster)
self:_AssignMission(Cluster)
end
function BlueIntel:onbeforeNewCluster(From,Event,To,Cluster)
AssignCluster(Cluster)
end
self.Intel = BlueIntel
return self
end
-------------------------------------------------------------------------
-- TODO FSM Functions
-------------------------------------------------------------------------
--- (Internal) FSM Function onafterStart
-- @param #EASYA2G self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYA2G self
function EASYA2G:onafterStart(From,Event,To)
self:T({From,Event,To})
self:_StartIntel()
self:_CreateAirwings()
self:_CreateSquads()
--self:_SetCAPPatrolPoints()
self:_SetTankerPatrolPoints()
self:_SetAwacsPatrolPoints()
self:_SetReconPatrolPoints()
self:__Status(-10)
return self
end
--- (Internal) FSM Function onafterStatus
-- @param #EASYA2G self
-- @param #string From
-- @param #string Event
-- @param #string To
-- @return #EASYA2G self
function EASYA2G:onafterStatus(From,Event,To)
self:T({From,Event,To})
-- cleanup
local cleaned = false
local cleanlist = {}
for _,_auftrag in pairs(self.ListOfAuftrag) do
local auftrag = _auftrag -- Ops.Auftrag#AUFTRAG
if auftrag and (not (auftrag:IsCancelled() or auftrag:IsDone() or auftrag:IsOver())) then
table.insert(cleanlist,auftrag)
cleaned = true
end
end
if cleaned == true then
self.ListOfAuftrag = nil
self.ListOfAuftrag = cleanlist
end
-- Gather Some Stats
local function counttable(tbl)
local count = 0
for _,_data in pairs(tbl) do
count = count + 1
end
return count
end
local wings = counttable(self.ManagedAW)
local squads = counttable(self.ManagedSQ)
local caps = counttable(self.ManagedCP)
local assets = 0
local instock = 0
local capmission = 0
local interceptmission = 0
local reconmission = 0
local awacsmission = 0
local tankermission = 0
local alert5mission = 0
for _,_wing in pairs(self.wings) do
local count = _wing[1]:CountAssetsOnMission(MissionTypes,Cohort)
local count2 = _wing[1]:CountAssets(true,MissionTypes,Attributes)
--capmission = capmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.PATROLRACETRACK})
interceptmission = interceptmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.BAI})
reconmission = reconmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.RECON})
awacsmission = awacsmission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.AWACS})
tankermission = tankermission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER})
alert5mission = alert5mission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.ALERT5})
assets = assets + count
instock = instock + count2
local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.BAI,AUFTRAG.Type.ALERT5})
-- update ready groups
self.ReadyFlightGroups = nil
self.ReadyFlightGroups = {}
for _,_asset in pairs(assetsonmission or {}) do
local asset = _asset -- Functional.Warehouse#WAREHOUSE.Assetitem
local FG = asset.flightgroup -- Ops.FlightGroup#FLIGHTGROUP
if FG then
local name = FG:GetName()
local engage = FG:IsEngaging()
local hasmissiles = FG:CanAirToGround()
--self:T("Is Alert5? "..tostring(FG:GetMissionCurrent().type))
local isalert5 = (FG:GetMissionCurrent() ~= nil and FG:GetMissionCurrent().type == AUFTRAG.Type.ALERT5) and true or false
local ready = hasmissiles and FG:IsFuelGood() and (FG:IsAirborne() or isalert5)
self:T(string.format("Flightgroup %s Engaging = %s Ready = %s (HasAmmo = %s HasFuel = %s Alert5 = %s)",tostring(name),tostring(engage),tostring(ready),tostring(hasmissiles),tostring(FG:IsFuelGood()), tostring(isalert5)))
if ready then
self.ReadyFlightGroups[name] = FG
end
end
end
end
if self.Monitor then
local threatcount = #self.Intel.Clusters or 0
local text = self.alias
text = text.."\nWings: "..wings.."\nSquads: "..squads.."\nHoldPoints: "..caps.."\nAssets on Mission: "..assets.."\nAssets in Stock: "..instock
text = text.."\nThreats: "..threatcount
text = text.."\nAirWing alive Missions: "..capmission+awacsmission+tankermission+reconmission+interceptmission+alert5mission
--text = text.."\n - A2G Holding: "..capmission
text = text.."\n - A2G Attack: "..interceptmission
text = text.."\n - AWACS: "..awacsmission
text = text.."\n - TANKER: "..tankermission
text = text.."\n - Recon: "..reconmission
text = text.."\n - Alert5 "..alert5mission
text = text.."\nMission Limit: "..self.MaxAliveMissions
MESSAGE:New(text,15,"A2G"):ToAll():ToLogIf(self.debug)
end
self:__Status(30)
return self
end
+141 -10
View File
@@ -1,3 +1,12 @@
--- **Ops** - Create your A2A Defenses.
--
-- **Main Features:**
--
-- * Automatically create and manage A2A defenses using an AirWing and Squadrons for one coalition
-- * Easy set-up
--
-- ===
--
-------------------------------------------------------------------------
-- Easy CAP/GCI Class, based on OPS classes
-------------------------------------------------------------------------
@@ -109,7 +118,7 @@
-- ### Prerequisites
--
-- You have to put a **STATIC WAREHOUSE** object on the airbase with the UNIT name according to the name of the airbase. **Do not put any other static type or it creates a conflict with the airbase name!**
-- E.g. for Kuitaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- E.g. for Kutaisi this has to have the unit name Kutaisi. This object symbolizes the AirWing HQ.
-- Next put a late activated template group for your CAP/GCI Squadron on the map. Last, put a zone on the map for the CAP operations, let's name it "Blue Zone 1". Size of the zone plays no role.
-- Put an EW radar system on the map and name it aptly, like "Blue EWR".
--
@@ -205,7 +214,7 @@ EASYGCICAP = {
coalition = "blue",
alias = nil,
wings = {},
Intel = nil,
Intel = nil, -- Ops.Intel#INTEL
resurrection = 900,
capspeed = 300,
capalt = 25000,
@@ -275,10 +284,11 @@ EASYGCICAP = {
--- EASYGCICAP class version.
-- @field #string version
EASYGCICAP.version="0.1.30"
EASYGCICAP.version="0.1.33"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
--
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: TBD
@@ -350,6 +360,34 @@ function EASYGCICAP:New(Alias, AirbaseName, Coalition, EWRName)
self:__Start(math.random(6,12))
--- On Before "Start" event.
-- @function [parent=#EASYGCICAP] OnBeforeStart
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Start" event.
-- @function [parent=#EASYGCICAP] OnAfterStart
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On Before "Status" event.
-- @function [parent=#EASYGCICAP] OnBeforeStatus
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
--- On After "Status" event.
-- @function [parent=#EASYGCICAP] OnAfterStatus
-- @param #EASYGCICAP self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
return self
end
@@ -369,6 +407,28 @@ function EASYGCICAP:GetAirwing(AirbaseName)
return nil
end
--- Add an agent to the underlying INTEL detection - caution, we need to be started first for this to work!
-- Normally this isn't necessary when the Group name is correctly filled (see EWRName in `New()`).
-- @param #EASYGCICAP self
-- @param Wrapper.Group#GROUP Group The group object to be added as Intel Agent.
-- @return #EASYGCICAP self
function EASYGCICAP:AddAgent(Group)
self:T(self.lid.."AddAgent")
if Group:IsInstanceOf("GROUP") and self.Intel ~= nil then
self.Intel:AddAgent(Group)
if self.TankerInvisible == true then
Group:SetCommandInvisible(true)
Group:OptionROEHoldFire()
if Group:IsAir() then
Group:OptionROTEvadeFire()
else
Group:OptionDisperseOnAttack(30)
end
end
end
return self
end
--- Get a table of all managed AirWings
-- @param #EASYGCICAP self
-- @return #table Table of Ops.AirWing#AIRWING Airwings
@@ -611,7 +671,6 @@ function EASYGCICAP:SetCapStartTimeVariation(Start, End)
return self
end
--- Set which target types CAP flights will prefer to engage, defaults to {"Air"}
-- @param #EASYGCICAP self
-- @param #table types Table of comma separated #string entries, defaults to {"Air"} (everything that flies and is not a weapon). Useful other options are e.g. {"Bombers"}, {"Fighters"},
@@ -656,7 +715,7 @@ function EASYGCICAP:_CreateAirwings()
return self
end
--- (internal) Create and add another AirWing to the manager
--- (Internal) Create and add another AirWing to the manager
-- @param #EASYGCICAP self
-- @param #string Airbasename
-- @param #string Alias
@@ -763,8 +822,13 @@ function EASYGCICAP:_AddAirwing(Airbasename, Alias)
end
end
if self.noalert5 > 0 then
local alert = AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT)
if self.noalert5 > 0 then
local alert
if self.ClassName == "EASYGCICAP" then
alert = AUFTRAG:NewALERT5(AUFTRAG.Type.INTERCEPT)
elseif self.ClassName == "EASYA2G" then
alert = AUFTRAG:NewALERT5(AUFTRAG.Type.BAI)
end
alert:SetRequiredAssets(self.noalert5)
alert:SetRepeat(99)
CAP_Wing:AddMission(alert)
@@ -1300,6 +1364,54 @@ function EASYGCICAP:AddConflictZone(Zone)
return self
end
--- Function to set corridor zones.
-- @param #EASYGCICAP self
-- @param Core.Set#SET_ZONE CorridorZones Can be handed in as SET\_ZONE or single ZONE object.
-- @return #EASYGCICAP self
function EASYGCICAP:SetCorridorZones(CorridorZones)
self:T(self.lid .. "SetCorridorZones")
if CorridorZones and CorridorZones:IsInstanceOf("SET_ZONE") then
self.corridorzones = CorridorZones
self.usecorridors = true
elseif CorridorZones and CorridorZones:IsInstanceOf("ZONE_BASE") then
if not self.corridorzones then self.corridorzones = SET_ZONE:New() end
self.corridorzones:AddZone(CorridorZones)
self.usecorridors = true
end
return self
end
--- Function to add one corridor zone.
-- @param #EASYGCICAP self
-- @param Core.Zone#ZONE CorridorZone The ZONE object to be added.
-- @return #EASYGCICAP self
function EASYGCICAP:AddCorridorZone(CorridorZone)
self:T(self.lid .. "AddCorridorZone")
self:SetCorridorZones(CorridorZone)
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #EASYGCICAP self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #EASYGCICAP self
function EASYGCICAP:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #EASYGCICAP self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #EASYGCICAP self
function EASYGCICAP:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
self.corridorfloor = Floor
self.corridorceiling = Ceiling
return self
end
--- (Internal) Try to assign the intercept to a FlightGroup already in air and ready.
-- @param #EASYGCICAP self
@@ -1459,6 +1571,8 @@ function EASYGCICAP:_AssignIntercept(Cluster)
if coord and conflictset:Count() > 0 and conflictset:IsCoordinateInZone(coord) then
success = false
end
else
success = true
end
return success
end,
@@ -1468,6 +1582,14 @@ function EASYGCICAP:_AssignIntercept(Cluster)
)
end
InterceptAuftrag:AddConditionFailure(
function()
local failure = false
if InterceptAuftrag:CountOpsGroups()==0 and InterceptAuftrag:IsExecuting() then failure = true end
return failure
end
)
table.insert(self.ListOfAuftrag,InterceptAuftrag)
local assigned, rest = self:_TryAssignIntercept(ReadyFlightGroups,InterceptAuftrag,contact.group,wingsize)
if not assigned then
@@ -1500,6 +1622,14 @@ function EASYGCICAP:_StartIntel()
BlueIntel:SetRejectZones(self.NoGoZoneSet)
BlueIntel:SetConflictZones(self.ConflictZoneSet)
BlueIntel:SetVerbosity(0)
if self.usecorridors == true then
BlueIntel:SetCorridorZones(self.corridorzones)
if self.corridorfloor or self.corridorceiling then
BlueIntel:SetCorridorLimitsFeet(self.corridorfloor,self.corridorceiling)
end
end
BlueIntel:Start()
if self.debug then
@@ -1603,7 +1733,7 @@ function EASYGCICAP:onafterStatus(From,Event,To)
tankermission = tankermission + _wing[1]:CountMissionsInQueue({AUFTRAG.Type.TANKER})
assets = assets + count
instock = instock + count2
local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})
local assetsonmission = _wing[1]:GetAssetsOnMission({AUFTRAG.Type.ALERT5, AUFTRAG.Type.GCICAP,AUFTRAG.Type.PATROLRACETRACK})
-- update ready groups
self.ReadyFlightGroups = nil
self.ReadyFlightGroups = {}
@@ -1614,7 +1744,8 @@ function EASYGCICAP:onafterStatus(From,Event,To)
local name = FG:GetName()
local engage = FG:IsEngaging()
local hasmissiles = FG:IsOutOfMissiles() == nil and true or false
local ready = hasmissiles and FG:IsFuelGood() and FG:IsAirborne()
local isalert5 = (FG:GetMissionCurrent() ~= nil and FG:GetMissionCurrent().type == AUFTRAG.Type.ALERT5) and true or false
local ready = hasmissiles and FG:IsFuelGood() and (FG:IsAirborne() or isalert5)
--self:T(string.format("Flightgroup %s Engaging = %s Ready = %s",tostring(name),tostring(engage),tostring(ready)))
if ready then
self.ReadyFlightGroups[name] = FG
+167 -5
View File
@@ -27,6 +27,9 @@
-- @field Core.Set#SET_ZONE acceptzoneset Set of accept zones. If defined, only contacts in these zones are considered.
-- @field Core.Set#SET_ZONE rejectzoneset Set of reject zones. Contacts in these zones are not considered, even if they are in accept zones.
-- @field Core.Set#SET_ZONE conflictzoneset Set of conflict zones. Contacts in these zones are considered, even if they are not in accept zones or if they are in reject zones.
-- @field Core.Set#SET_ZONE corridorzoneset Set of corridor zones. Contacts in these zones are never considered. Also see corridorfloorheight and corridorfloorceiling.
-- @field #number corridorfloor [Air] Contacts below this height (ASL!) are considered, even if they are in a corridor zone.
-- @field #number corridorceiling [Air] Contacts above this height (ASL!) are considered, even if they are in a corridor zone.
-- @field #table Contacts Table of detected items.
-- @field #table ContactsLost Table of lost detected items.
-- @field #table ContactsUnknown Table of new detected items.
@@ -39,6 +42,9 @@
-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition.
-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`.
-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative.
-- @field #boolean DetectAccoustic If true, also detect by sound (ie proximity).
-- @field #number DetectAccousticRadius Radius dfor accoustic detection, defaults to 2000 meters.
-- @field #table DetectAccousticUnitTypes Types of units we can detect accousticly. Defaults to {Unit.Category.HELICOPTER}
-- @extends Core.Fsm#FSM
--- Top Secret!
@@ -102,6 +108,9 @@ INTEL = {
clusterarrows = false,
prediction = 300,
detectStatics = false,
DetectAccoustic = false,
DetectAccousticRadius = 1000,
DetectAccousticUnitTypes = {Unit.Category.HELICOPTER},
}
--- Detected item info.
@@ -160,14 +169,14 @@ INTEL.Ctype={
--- INTEL class version.
-- @field #string version
INTEL.version="0.3.6"
INTEL.version="0.3.10"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Add min cluster size. Only create new clusters if they have a certain group size.
-- TODO: process detected set asynchroniously for better performance.
-- NODO: process detected set asynchroniously for better performance.
-- DONE: Add statics.
-- DONE: Filter detection methods.
-- DONE: Accept zones.
@@ -175,6 +184,7 @@ INTEL.version="0.3.6"
-- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone.
-- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?!
-- DONE: Combine units to groups for all, new and lost.
-- DONE: Add corridor zones.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
@@ -266,6 +276,7 @@ function INTEL:New(DetectionSet, Coalition, Alias)
self:SetForgetTime()
self:SetAcceptZones()
self:SetRejectZones()
self:SetCorridorZones()
self:SetConflictZones()
------------------------
@@ -398,6 +409,26 @@ function INTEL:SetAcceptZones(AcceptZoneSet)
return self
end
--- Set to accept accoustic detection.
-- @param #INTEL self
-- @param #number Radius Radius in which we can "hear" units. Defaults to 1000 meters.
-- @param #table UnitCategories Set what Unit Categories we can "hear". Defaults to `{Unit.Category.GROUND_UNIT,Unit.Category.HELICOPTER}`
-- @return #INTEL self
function INTEL:SetAccousticDetectionOn(Radius,UnitCategories)
self.DetectAccoustic = true
self.DetectAccousticRadius = Radius or 1000
self.DetectAccousticUnitTypes = UnitCategories or {Unit.Category.HELICOPTER}
return self
end
--- Switch off accoustic detection.
-- @param #INTEL self
-- @return #INTEL self
function INTEL:SetAccousticDetectionOff()
self.DetectAccoustic = false
return self
end
--- Add an accept zone. Only contacts detected in this zone are considered.
-- @param #INTEL self
-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set.
@@ -475,7 +506,69 @@ function INTEL:RemoveConflictZone(ConflictZone)
return self
end
--- **OBSOLETE, will be removed in next version!** Set forget contacts time interval.
--- Set corrdidor zones. Contacts detected in this/these zone(s) are never reported by the detection.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE CorridorZoneSet Set of corrdidor zone(s).
-- @return #INTEL self
function INTEL:SetCorridorZones(CorridorZoneSet)
self.corridorzoneset=CorridorZoneSet or SET_ZONE:New()
return self
end
--- Add a corrdidor zone. Contacts detected in this zone are corrdidored and not reported by the detection.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corrdidor zone set.
-- @return #INTEL self
function INTEL:AddCorridorZone(CorridorZone)
self.corridorzoneset:AddZone(CorridorZone)
return self
end
--- Remove a corrdidor zone from the corrdidor zone set.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Zone#ZONE CorridorZone Remove a zone from the corrdidor zone set.
-- @return #INTEL self
function INTEL:RemoveCorridorZone(CorridorZone)
self.corridorzoneset:Remove(CorridorZone:GetName(), true)
return self
end
--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values.
-- Overrides corridor exception for objects flying outside this limitations.
-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object:
-- `mycorridorzone:SetProperty("CorridorFloor",500)` -- meters, case sensitivity matters!
-- `mycorridorzone:SetProperty("CorridorCeiling",10000)` -- meters, case sensitivity matters!
-- @param #INTEL self
-- @param #number Floor Floor altitude in meters.
-- @param #number Ceiling Ceiling altitude in meters.
-- @return #INTEL self
function INTEL:SetCorridorLimits(Floor,Ceiling)
self.corridorceiling = Ceiling or 10000
self.corridorfloor = Floor or 1
return self
end
--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values.
-- Overrides corridor exception for objects flying outside this limitations.
-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object:
-- `mycorridorzone:SetProperty("CorridorFloor",UTILS.FeetToMeters(5000))` -- feet, case sensitivity matters!
-- `mycorridorzone:SetProperty("CorridorCeiling",UTILS.FeetToMeters(20000))` -- feet, case sensitivity matters!
-- @param #INTEL self
-- @param #number Floor Floor altitude in feet.
-- @param #number Ceiling Ceiling altitude in feet.
-- @return #INTEL self
function INTEL:SetCorridorLimitsFeet(Floor,Ceiling)
local Ceiling = Ceiling or 25000
local Floor = Floor or 15000
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
return self
end
--- **OBSOLETE, not functional!** Set forget contacts time interval.
-- Previously known contacts that are not detected any more, are "lost" after this time.
-- This avoids fast oscillations between a contact being detected and undetected.
-- @param #INTEL self
@@ -831,6 +924,18 @@ function INTEL:UpdateIntel()
self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK)
end
if self.DetectAccoustic then
local recce = group:GetFirstUnitAlive()
local detectionzone = group:GetProperty("INTEL_DETECT_ACCZONE")
if not detectionzone then
detectionzone = ZONE_GROUP:New(group.IdentifiableName.."INTEL_DETECT_ACCZONE",group,self.DetectAccousticRadius or 2000)
group:SetProperty("INTEL_DETECT_ACCZONE",detectionzone)
end
if recce and recce:IsGround() then
self:GetDetectedUnitsAccoustic(recce,DetectedUnits,RecceDetecting,detectionzone)
end
end
end
end
@@ -884,6 +989,36 @@ function INTEL:UpdateIntel()
table.insert(remove, unitname)
end
end
-- Check if unit is in any of the corridor zones.
if self.corridorzoneset:Count()>0 then
self:T("Corridorzone Check for unit "..unit:GetName())
local inzone = false
for _,_zone in pairs(self.corridorzoneset.Set) do
local zone=_zone --Core.Zone#ZONE
if unit:IsInZone(zone) then
local corridorfloor = zone:GetProperty("CorridorFloor") or self.corridorfloor
local corridorceiling = zone:GetProperty("CorridorCeiling") or self.corridorceiling
local debugtext = "Corridorzone Check for unit "..unit:GetName().."\n"
debugtext = debugtext .. string.format("IsAir %s | Alt %dft | Floor %dft | Ceil %dft",tostring(unit:IsAir()),tonumber(UTILS.MetersToFeet(unit:GetAltitude())),
tonumber(UTILS.MetersToFeet(corridorfloor)),tonumber(UTILS.MetersToFeet(corridorceiling)))
MESSAGE:New(debugtext,15,"INTEL"):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1)
if unit:IsAir() and (corridorfloor ~= nil or corridorceiling ~= nil) then
local alt = unit:GetAltitude()
if corridorfloor and alt > corridorfloor then inzone = true end
if corridorceiling and (inzone == true or corridorfloor == nil) and alt < corridorceiling then inzone = true else inzone = false end
if inzone == true then break end
else
inzone=true
break
end
end
end
-- Unit is inside a corridor zone ==> remove!
if inzone then
table.insert(remove, unitname)
end
end
-- Filter unit categories. Added check that we have a UNIT and not a STATIC object because :GetUnitCategory() is only available for units.
if #self.filterCategory>0 and unit:IsInstanceOf("UNIT") then
@@ -1106,7 +1241,7 @@ function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecti
return self
end
--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
--- (Internal) Return the detected target groups of the controllable as a table.
-- The optional parameters specify the detection methods that can be applied.
-- If no detection method is given, the detection will use all the available methods by default.
-- @param #INTEL self
@@ -1202,6 +1337,33 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisua
end
end
--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT Recce The unit detecting.
-- @param #table DetectedUnits Table of detected units to be filled.
-- @param #table RecceDetecting Table of recce per unit to be filled.
-- @param Core.Zone#ZONE_GROUP detectionzone The zone where to look.
function INTEL:GetDetectedUnitsAccoustic(Recce,DetectedUnits,RecceDetecting,detectionzone)
local othercoalition = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
self:T("Other coalition = "..othercoalition)
if detectionzone then
-- Get detected units
local reccename = Recce:GetName()
local DetectAccousticUnitTypes = self.DetectAccousticUnitTypes or {Unit.Category.HELICOPTER}
detectionzone:Scan({Object.Category.UNIT},DetectAccousticUnitTypes)
local unitset = detectionzone:GetScannedSetUnit(othercoalition) -- Core.Set#SET_UNIT
self:T("Accoustic detection found #Units "..unitset:CountAlive())
for _,_unit in pairs(unitset.Set or {}) do
if _unit and _unit:IsAlive() and _unit:GetCoalition() ~= self.coalition then
local name = _unit:GetName() or "none"
DetectedUnits[name]=_unit
RecceDetecting[name]=reccename
self:T("Unit name = "..name)
end
end
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
@@ -2144,7 +2306,7 @@ function INTEL:GetClusterCoordinate(Cluster, Update)
return Cluster.coordinate
end
--- Check if the coorindate of the cluster changed.
--- Check if the coordindate of the cluster changed.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #number Threshold in meters. Default 100 m.
+9
View File
@@ -2741,6 +2741,15 @@ function OPSGROUP:IsOutOfTorpedos()
return self.outofTorpedos
end
--- Check if the group is out of A2G Ammo
-- @param #OPSGROUP self
-- @return #boolean If `true`, group is out of torpedos.
function OPSGROUP:IsOutOfA2GAmmo()
if (self.outofMissilesAG and self.outofBombs and self.outofGuns) or self.outofAmmo then
return true
end
return false
end
--- Check if the group has currently switched a LASER on.
-- @param #OPSGROUP self
+313 -28
View File
@@ -21,7 +21,7 @@
-- ===
-- @module Ops.PlayerTask
-- @image OPS_PlayerTask.jpg
-- @date Last Update Oct 2025
-- @date Last Update Dec 2025
do
@@ -61,6 +61,7 @@ do
-- @field #number PreviousCount
-- @field #boolean CanSmoke
-- @field #boolean ShowThreatDetails
-- @field #boolean PersistMe
-- @extends Core.Fsm#FSM
@@ -98,11 +99,12 @@ PLAYERTASK = {
PreviousCount = 0,
CanSmoke = true,
ShowThreatDetails = true,
PersistMe = false,
}
--- PLAYERTASK class version.
-- @field #string version
PLAYERTASK.version="0.1.29"
PLAYERTASK.version="0.1.31"
--- Generic task condition.
-- @type PLAYERTASK.Condition
@@ -401,8 +403,17 @@ function PLAYERTASK:CanJoinTask(Group, Client)
return true
end
--- [User] Set this task for persistance, if persistance is enabled on the PLAYERTASKCONTROLLER instance.
-- @param #PLAYERTASK self
-- @return #PLAYERTASK self
function PLAYERTASK:EnablePersistance()
self.PersistMe = true
return self
end
--- [Internal] Add a PLAYERTASKCONTROLLER for this task
-- @param #PLAYERTASK self
--
-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller
-- @return #PLAYERTASK self
function PLAYERTASK:_SetController(Controller)
@@ -1088,7 +1099,6 @@ function PLAYERTASK:onafterStatus(From, Event, To)
return self
end
--- [Internal] On after progress call
-- @param #PLAYERTASK self
-- @param #string From
@@ -1363,6 +1373,12 @@ do
-- @field Core.ClientMenu#CLIENTMENU MenuNoTask
-- @field #boolean InformationMenu Show Radio Info Menu
-- @field #number TaskInfoDuration How long to show the briefing info on the screen
-- @field #table TaskPersistance Table for persistance data
-- @field #boolean TaskPersistanceSwitch Switch for persisting tasks
-- @field #string TaskPersistancePath File path for persisting tasks
-- @field #string TaskPersistanceFilename File name for persisting tasks
-- @field #table TasksPersistable List of persistable tasks
-- @field #number SceneryExplosivesAmount Kgs of TNT to explode scenery on task persistance loading
-- @extends Core.Fsm#FSM
---
@@ -1669,8 +1685,28 @@ do
--
-- Set a marker on the map and add the following text to create targets from it: "TARGET". This is effectively the same as adding a COORDINATE object as target.
-- The marker can be deleted any time.
--
-- ## 9 Discussion
--
-- ## 9 Single Task Persistence for mission designer added tasks
--
-- The class can persist the state of single tasks of type BOMBING, PRECISIONBOMBING, ARTY and SEAD, i.e. tasks which have a GROUND(!) GROUP, UNIT, STATIC or SCENERY as target.
-- This requires the task to have a unique(!) menu name set, a TARGET which already exists on the map at mission start(!), and a flag that this task is actually to be persisted.
-- Also, you need to desanitize the mission scripting environment, i.e. "lfs" and "io" must be available so we can write to disk.
--
-- -- First, we need to enable on the PLAYERTASKCONTROLLER itself
-- taskmanager:EnableTaskPersistance([[C:\Users\myname\Saved Games\DCS\Missions\MyMisionFolder\]],"Mission Tasks.csv") -- Path and Filename
--
-- -- Then, we can design a task marking mission progress that we want to persist
-- local RussianRadios = SET_STATIC:New():FilterPrefixes("Comms Tower Russia"):FilterOnce()
--
-- local RadioTask = PLAYERTASK:New(AUFTRAG.Type.BOMBING,RussianRadios,true,5,"Bombing")
-- RadioTask:SetMenuName("Neutralize Comms Towers") -- UNIQUE menu name so we can find the task later!
-- RadioTask:AddFreetext("Find and neutralize the two communication towers near NB70 East of Fulda on Streufelsberg!")
-- RadioTask:AddFreetextTTS("Find and neutralize the two communication towers naer N;B;7;zero; East of Fulda on Streufelsberg!")
-- RadioTask:EnablePersistance() -- Enable persistence for this task
--
-- taskmanager:AddPlayerTaskToQueue(RadioTask,true,false)
--
-- ## 10 Discussion
--
-- If you have questions or suggestions, please visit the [MOOSE Discord](https://discord.gg/AeYAkHP) #ops-playertask channel.
--
@@ -1720,6 +1756,12 @@ PLAYERTASKCONTROLLER = {
MenuNoTask = nil,
InformationMenu = false,
TaskInfoDuration = 30,
TaskPersistance = {},
TaskPersistanceSwitch = false,
TaskPersistancePath = nil,
TaskPersistanceFilename = nil,
TasksPersistable = {},
SceneryExplosivesAmount = 300,
}
---
@@ -1740,6 +1782,7 @@ AUFTRAG.Type.PRECISIONBOMBING = "Precision Bombing"
AUFTRAG.Type.CTLD = "Combat Transport"
AUFTRAG.Type.CSAR = "Combat Rescue"
AUFTRAG.Type.CONQUER = "Conquer"
---
-- @type Scores
PLAYERTASKCONTROLLER.Scores = {
@@ -1759,6 +1802,24 @@ PLAYERTASKCONTROLLER.Scores = {
[AUFTRAG.Type.CAP] = 100,
[AUFTRAG.Type.CAPTUREZONE] = 100,
}
---
-- @type TasksPersistable
PLAYERTASKCONTROLLER.TasksPersistable = {
[AUFTRAG.Type.PRECISIONBOMBING] = true,
[AUFTRAG.Type.BOMBING] = true,
[AUFTRAG.Type.ARTY] = true,
[AUFTRAG.Type.SEAD] = true,
}
---
-- @type PersistenceData
-- @field #number ID
-- @field #string Name
-- @field #string Type
-- @field #number InitialTargets
-- @field #number Targetsleft
-- @field #boolean updated
---
-- @type SeadAttributes
@@ -2059,7 +2120,7 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
self:AddTransition("*", "Stop", "Stopped")
self:__Start(2)
local starttime = math.random(5,10)
local starttime = math.random(10,15)
self:__Status(starttime)
self:I(self.lid..self.version.." Started.")
@@ -2174,6 +2235,30 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter)
end
--- [User] Enable Task persistance (for specific gound target tasks)
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Path Path where to save the task data
-- @param #string Filename File name under which to save the task data
-- @param #number KgsOfTNT (Optional) Explosives kgs used to remove scenery for persistence, defaults to 300
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:EnableTaskPersistance(Path,Filename,KgsOfTNT)
self.TaskPersistanceSwitch = true
self.TaskPersistancePath = Path
self.TaskPersistanceFilename = Filename
self.SceneryExplosivesAmount = KgsOfTNT or 300
return self
end
--- [User] Disable Task persistance (for specific gound target tasks)
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:DisableTaskPersistance()
self.TaskPersistanceSwitch = false
self.TaskPersistancePath = nil
self.TaskPersistanceFilename = nil
return self
end
--- [User] Set or create a SCORING object for this taskcontroller
-- @param #PLAYERTASKCONTROLLER self
-- @param Functional.Scoring#SCORING Scoring (optional) the Scoring object
@@ -2861,15 +2946,6 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType()
end
end
--[[
for _type,_data in pairs(tasktypes) do
self:I("Task Type: ".._type)
for _id,_task in pairs(_data) do
self:I("Task Name: ".._task.Target:GetName())
end
end
--]]
return tasktypes
end
@@ -3566,9 +3642,9 @@ function PLAYERTASKCONTROLLER:AddPlayerTaskToQueue(PlayerTask,Silent,TaskFilter)
PlayerTask:_SetController(self)
PlayerTask:SetCoalition(self.Coalition)
self.TaskQueue:Push(PlayerTask)
if not Silent then
self:__TaskAdded(10,PlayerTask)
end
--if not Silent then
self:__TaskAdded(10,PlayerTask,Silent)
--end
else
self:E(self.lid.."***** NO valid PAYERTASK object sent!")
end
@@ -3711,7 +3787,7 @@ function PLAYERTASKCONTROLLER:_FlashInfo()
local task = self.TasksPerPlayer:ReadByID(_playername) -- Ops.PlayerTask#PLAYERTASK
local Coordinate = task.Target:GetCoordinate()
local CoordText = ""
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then
CoordText = Coordinate:ToStringA2G(_client, nil, self.ShowMagnetic)
else
CoordText = Coordinate:ToStringA2A(_client, nil, self.ShowMagnetic)
@@ -3770,7 +3846,7 @@ function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client)
local CoordTextLLDM = nil
local ShowThreatInfo = task.ShowThreatDetails
local LasingDrone = self:_FindLasingDroneForTaskID(task.PlayerTaskNr)
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and task.Type~=AUFTRAG.Type.INTERCEPT then
CoordText = Coordinate:ToStringA2G(Client,nil,self.ShowMagnetic)
else
CoordText = Coordinate:ToStringA2A(Client,nil,self.ShowMagnetic)
@@ -4584,6 +4660,76 @@ function PLAYERTASKCONTROLLER:RemoveConflictZone(ConflictZone)
return self
end
--- [User] Add an corridor zone to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corridor zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddCorridorZone(CorridorZone)
self:T(self.lid.."AddCorridorZone")
if self.Intel then
self.Intel:AddCorridorZone(CorridorZone)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- [User] Add an corridor SET_ZONE to INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Set#SET_ZONE CorridorZoneSet Add a SET_ZONE to the corridor zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:AddCorridorZoneSet(CorridorZoneSet)
self:T(self.lid.."AddCorridorZoneSet")
if self.Intel then
self.Intel.corridorzoneset:AddSet(CorridorZoneSet)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- [User] Remove an corridor zone from INTEL detection. You need to set up detection with @{#PLAYERTASKCONTROLLER.SetupIntel}() **before** using this.
-- @param #PLAYERTASKCONTROLLER self
-- @param Core.Zone#ZONE CorridorZone Remove this zone from the corridor zone set.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:RemoveCorridorZone(CorridorZone)
self:T(self.lid.."RemoveCorridorZone")
if self.Intel then
self.Intel:RemoveCorridorZone(CorridorZone)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- Function to set corridor zone floor and ceiling in FEET.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Floor Floor altitude ASL in feet.
-- @param #number Ceiling Ceiling altitude ASL in feet.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeiling(Floor,Ceiling)
if self.Intel then
self.Intel:SetCorridorLimitsFeet(Floor,Ceiling)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- Function to set corridor zone floor and ceiling in METERS.
-- @param #PLAYERTASKCONTROLLER self
-- @param #number Floor Floor altitude ASL in meters.
-- @param #number Ceiling Ceiling altitude ASL in meters.
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:SetCorridorZoneFloorAndCeilingMeters(Floor,Ceiling)
if self.Intel then
self.Intel:SetCorridorLimits(Floor,Ceiling)
else
self:E(self.lid.."*****NO detection has been set up (yet)!")
end
return self
end
--- [User] Set the top menu name to a custom string.
-- @param #PLAYERTASKCONTROLLER self
-- @param #string Name The name to use as the top menu designation.
@@ -4765,6 +4911,128 @@ function PLAYERTASKCONTROLLER:SetSRSBroadcast(Frequency,Modulation)
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
-- @param Ops.PlayerTask#PLASERTASK Task
-- @param #number TargetsLeft
function PLAYERTASKCONTROLLER:_UpdateTargetsAlive(Task,TargetsLeft)
self:T(self.lid.."_UpdateTargetsAlive")
local delta = Task.Target:CountTargets() - TargetsLeft
if delta > 0 then
self:T("Delta targets to be removed: "..delta)
local count = 0
local targets = Task.Target:GetObjects()
for _,_object in pairs(targets or {}) do
if _object and _object.ClassName and (_object:IsInstanceOf("GROUP") or _object:IsInstanceOf("UNIT") or _object:IsInstanceOf("STATIC") or _object:IsInstanceOf("SCENERY")) then
if count < delta then
count = count + 1
if not _object:IsInstanceOf("SCENERY") then
_object:Destroy(true)
else
_object:Explode(self.SceneryExplosivesAmount)
end
end
end
end
end
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_LoadTasksPersisted()
self:T(self.lid.."_LoadTasksPersisted")
local function MatchTask(Type,Name)
local foundtask
self.TaskQueue:ForEach(
function(_task)
local task = _task -- #PLAYERTASK
if task.Type == Type and task.Target.name and task.Target.name == Name then
foundtask = task
end
end
)
return foundtask
end
if lfs and io then
local ok,data = UTILS.LoadFromFile(self.TaskPersistancePath,self.TaskPersistanceFilename)
if ok == true then
table.remove(data, 1)
for _,_entry in pairs(data) do
-- "--ID;;Name;;InitialTargets;;Targetsleft;;Type\n"
local dataset = UTILS.Split(_entry,";;")
local Taskdata = {} -- #PersistenceData
Taskdata.ID = tonumber(dataset[1])
Taskdata.Name = tostring(dataset[2])
Taskdata.InitialTargets = tonumber(dataset[3])
Taskdata.Targetsleft = tonumber(dataset[4])
Taskdata.Type = tostring(dataset[5])
Taskdata.Task = MatchTask(Taskdata.Type,Taskdata.Name)
if Taskdata.Task == nil then
self:E(self.lid.."No actual task found for "..Taskdata.Name)
else
self:T(self.lid.."Task loaded and match found for "..Taskdata.Name)
end
Taskdata.updated = Taskdata.InitialTargets == Taskdata.Targetsleft and true or false
if Taskdata.Task and Taskdata.updated == false then
self:_UpdateTargetsAlive(Taskdata.Task,Taskdata.Targetsleft)
Taskdata.updated = true
end
self.TaskPersistance[Taskdata.ID] = Taskdata
end
end
end
return self
end
--- [User] Clear persisted data on disk.
-- @param #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:ClearPersistedData()
if lfs and io then
local text = "-- Data Cleared\n"
UTILS.SaveToFile(self.TaskPersistancePath,self.TaskPersistanceFilename,text)
end
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:_SaveTasksPersisted()
if lfs and io then
local text = "--ID;;Name;;InitialTargets;;Targetsleft;;Type\n"
for _,_data in pairs(self.TaskPersistance) do
local data = _data -- #PersistenceData
data.Targetsleft = data.Task.Target:CountTargets() -- recount
if data.Task and data.Task:IsDone() then data.Targetsleft = 0 end
local tasktext = string.format("%d;;%s;;%d;;%d;;%s\n",data.ID,data.Name,data.InitialTargets,data.Targetsleft,data.Type)
text = text..tasktext
end
UTILS.SaveToFile(self.TaskPersistancePath,self.TaskPersistanceFilename,text)
end
return self
end
---
-- @param #PLAYERTASKCONTROLLER self
-- @param #PLAYERTASK Task
function PLAYERTASKCONTROLLER:_AddPersistenceData(Task)
local Taskdata = {} -- #PersistenceData
if not self.TaskPersistance[Task.PlayerTaskNr] then
Taskdata.ID = Task.PlayerTaskNr
Taskdata.Name = Task.Target.name or "none"
Taskdata.InitialTargets = Task.Target:CountTargets()
Taskdata.Targetsleft = Taskdata.InitialTargets
Taskdata.Type = Task.Type
Taskdata.updated = true
Taskdata.Task = Task
self.TaskPersistance[Task.PlayerTaskNr] = Taskdata
end
return self
end
-------------------------------------------------------------------------------------------------------------------
-- FSM Functions PLAYERTASKCONTROLLER
-- TODO: FSM Functions PLAYERTASKCONTROLLER
@@ -4788,7 +5056,12 @@ function PLAYERTASKCONTROLLER:onafterStart(From, Event, To)
self:HandleEvent(EVENTS.PilotDead, self._EventHandler)
self:HandleEvent(EVENTS.PlayerEnterAircraft, self._EventHandler)
self:HandleEvent(EVENTS.UnitLost, self._EventHandler)
self:SetEventPriority(5)
self:SetEventPriority(5)
-- Persistence
if self.TaskPersistanceSwitch == true then
self:ScheduleOnce(5,self._LoadTasksPersisted,self)
--self:_LoadTasksPersisted()
end
return self
end
@@ -4828,6 +5101,11 @@ function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To)
self:I(text)
end
-- Persistence
if self.TaskPersistanceSwitch == true then
self:_SaveTasksPersisted()
end
if self:GetState() ~= "Stopped" then
self:__Status(-30)
end
@@ -4983,19 +5261,26 @@ end
-- @param #string Event
-- @param #string To
-- @param Ops.PlayerTask#PLAYERTASK Task
-- @param #boolean Silent
-- @return #PLAYERTASKCONTROLLER self
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task)
function PLAYERTASKCONTROLLER:onafterTaskAdded(From, Event, To, Task, Silent)
self:T({From, Event, To})
self:T(self.lid.."TaskAdded")
local addtxt = self.gettext:GetEntry("TASKADDED",self.locale)
local taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.Type))
if not self.NoScreenOutput then
self:_SendMessageToClients(taskname,15)
--local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
if not Silent then
if not self.NoScreenOutput then
self:_SendMessageToClients(taskname,15)
--local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition)
end
if self.UseSRS then
taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
end
end
if self.UseSRS then
taskname = string.format(addtxt, self.MenuName or self.Name, tostring(Task.TTSType))
self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2)
self:T(self.lid..string.format("Pers = %s | Type = %s | TypePers = %s | TaskFlag = %s",tostring(self.TaskPersistanceSwitch),tostring(Task.Type),tostring(self.TasksPersistable[Task.Type]),tostring(Task.PersistMe)))
if self.TaskPersistanceSwitch == true and self.TasksPersistable[Task.Type] == true and Task.PersistMe == true then
self:_AddPersistenceData(Task)
end
return self
end
+4 -5
View File
@@ -884,7 +884,7 @@ function RESCUEHELO:onafterStart(From, Event, To)
else
-- Spawn helo. We need to introduce an alias in case this class is used twice. This would confuse the spawn routine.
local Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias)
Spawn=SPAWN:NewWithAlias(self.helogroupname, self.alias)
-- Set modex for spawn.
Spawn:InitModex(self.modex)
@@ -933,8 +933,8 @@ function RESCUEHELO:onafterStart(From, Event, To)
self:E(string.format("ERROR: No uncontrolled (alive) rescue helo group with name %s could be found!", self.helogroupname))
return
end
elseif UsesAliveGroup==false then
end
elseif UsesAliveGroup==false then
-- Spawn at airbase.
self.helo=Spawn:SpawnAtAirbase(self.airbase, self.takeoff, nil, AIRBASE.TerminalType.HelicopterUsable)
@@ -948,9 +948,8 @@ function RESCUEHELO:onafterStart(From, Event, To)
delay=60
end
end
end
-- Set of group(s) to follow Mother.
self.followset=SET_GROUP:New()
+26 -1
View File
@@ -515,7 +515,8 @@ ENUMS.ReportingName =
Atlas = "A400",
Lancer = "B1-B",
Stratofortress = "B-52H",
Hercules = "C-130",
Herc = "C-130",
Hercules = "C-130J-30",
Super_Hercules = "Hercules",
Globemaster = "C-17",
Greyhound = "C-2A",
@@ -1772,6 +1773,30 @@ ENUMS.Storage.weapons.adapters.lau105 = 'weapons.adapters.lau-105'
ENUMS.Storage.weapons.containers.US_M10_SMOKE_TANK_WHITE = 'weapons.containers.{US_M10_SMOKE_TANK_WHITE}'
ENUMS.Storage.weapons.bombs.Mk_82 = 'weapons.bombs.Mk_82'
ENUMS.Storage.weapons.adapters.BRU42_LS_SUU25 = 'weapons.adapters.BRU-42_LS_(SUU-25)'
ENUMS.Storage.weapons.missiles.Aster_30_Blk_1 = 'weapons.missiles.Aster 30 Blk 1'
ENUMS.Storage.weapons.missiles.Aster_30_Blk_1NT = 'weapons.missiles.Aster 30 Blk 1NT'
ENUMS.Storage.weapons.missiles.Aster_30_Blk_2 = 'weapons.missiles.Aster 30 Blk 2'
ENUMS.Storage.weapons.missiles.SA9M83M = 'weapons.missiles.SA9M83M'
ENUMS.Storage.weapons.gunmounts.C130_M4_Rifle = 'weapons.gunmounts.C130_M4_Rifle'
ENUMS.Storage.weapons.gunmounts.C130_M18_Sidearm_ = 'weapons.gunmounts.{C130-M18-Sidearm}'
ENUMS.Storage.weapons.gunmounts.C130_Cargo_Bay_M4 = 'weapons.gunmounts.{C130-Cargo-Bay-M4}'
ENUMS.Storage.weapons.gunmounts.C130_M18_Sidearm = 'weapons.gunmounts.C130_M18_Sidearm'
ENUMS.Storage.weapons.droptanks.C130J_Ext_Tank_R = 'weapons.droptanks.C130J_Ext_Tank_R'
ENUMS.Storage.weapons.droptanks.C130J_Ext_Tank_L = 'weapons.droptanks.C130J_Ext_Tank_L'
ENUMS.Storage.weapons.missiles.SAHQ2 = 'weapons.missiles.SAHQ2'
ENUMS.Storage.weapons.missiles.Strela_2 = 'weapons.missiles.Strela-2'
ENUMS.Storage.weapons.missiles.Strela_2M = 'weapons.missiles.Strela-2M'
ENUMS.Storage.weapons.missiles.Strela_3 = 'weapons.missiles.Strela-3'
ENUMS.Storage.weapons.missiles.SA9M83 = 'weapons.missiles.SA9M83'
ENUMS.Storage.weapons.missiles.SAV601P = 'weapons.missiles.SAV601P'
ENUMS.Storage.weapons.missiles.SA2V759 = 'weapons.missiles.SA2V759'
ENUMS.Storage.weapons.missiles.SA9M317 = 'weapons.missiles.SA9M317'
ENUMS.Storage.weapons.missiles.SA9M82M = 'weapons.missiles.SA9M82M'
ENUMS.Storage.weapons.missiles.SA9M82 = 'weapons.missiles.SA9M82'
ENUMS.Storage.weapons.missiles.Igla_S = 'weapons.missiles.Igla_S'
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.{AKAN_NO_TRC}'
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.{AKAN}'
ENUMS.Storage.weapons.shells.M882_9x19 = 'weapons.shells.9x19_m882'
-- UH-60L Mod
ENUMS.Storage.weapons.gunmounts.UH60LGAU19 = 'weapons.gunmounts.UH-60L GAU-19'
+205 -23
View File
@@ -249,7 +249,23 @@ CALLSIGN={
Stetson = 22,
Wrath = 23,
},
Intruder = {
Raygun = 4,
Heartless = 5,
Viceroy = 6,
Cupcake = 7,
["Flying Tiger"] = 8,
["Flying Ace"] = 9,
Buckeye = 10,
Goldplate = 11,
Phoenix = 12,
Electron = 13,
Rustler = 14,
Vixen = 15,
Jackal = 16,
Milestone = 17,
Devil = 18,
},
} --#CALLSIGN
--- Utilities static class.
@@ -2014,6 +2030,12 @@ function UTILS.GetCallsignName(Callsign)
end
end
for name, value in pairs(CALLSIGN.Intruder) do
if value==Callsign then
return name
end
end
return "Ghostrider"
end
@@ -2300,6 +2322,21 @@ function UTILS.IsLoadingDoorOpen( unit_name )
return true
end
if type_name=="C-130J-30" and (unit:getDrawArgumentValue(86)==1)then
BASE:T(unit_name.." rear doors are open")
return true
end
if type_name=="C-130J-30" and (unit:getDrawArgumentValue(87)==1)then
BASE:T(unit_name.." Side door(s) are open")
return true
end
if type_name=="C-130J-30" and (unit:getDrawArgumentValue(88)==1)then
BASE:T(unit_name.." Paratroop door(s) are open")
return true
end
if string.find(type_name, "Hercules") and (unit:getDrawArgumentValue(1215) == 1 and unit:getDrawArgumentValue(1216) == 1) then
BASE:T(unit_name .. " rear doors are open")
return true
@@ -4222,25 +4259,27 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,
local function PopulateStorage(Name,liquids,equip,airframes)
local newWH = STORAGE:New(Name)
if liquids and liquids > 0 then
-- Storage fill-up
newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) -- kgs to tons
newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids)
newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids)
newWH:SetLiquid(STORAGE.Liquid.MW50,liquids)
end
if equip and equip > 0 then
for cat,nitem in pairs(ENUMS.Storage.weapons) do
for name,item in pairs(nitem) do
newWH:SetItem(item,equip)
if newWH then
if liquids and liquids > 0 then
-- Storage fill-up
newWH:SetLiquid(STORAGE.Liquid.DIESEL,liquids) -- kgs to tons
newWH:SetLiquid(STORAGE.Liquid.GASOLINE,liquids)
newWH:SetLiquid(STORAGE.Liquid.JETFUEL,liquids)
newWH:SetLiquid(STORAGE.Liquid.MW50,liquids)
end
if equip and equip > 0 then
for cat,nitem in pairs(ENUMS.Storage.weapons) do
for name,item in pairs(nitem) do
newWH:SetItem(item,equip)
end
end
end
end
if airframes and airframes > 0 then
for typename in pairs (CSAR.AircraftType) do
newWH:SetItem(typename,airframes)
if airframes and airframes > 0 then
for typename in pairs (CSAR.AircraftType) do
newWH:SetItem(typename,airframes)
end
end
end
end
@@ -4319,8 +4358,6 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,
}
-- Create BIRTH event.
world.onEvent(Event)
PopulateStorage(Name.."-1",liquids,equip,airframes)
else
-- Spawn FARP
local newfarp = SPAWNSTATIC:NewFromType(STypeName,"Heliports",Country) -- "Invisible FARP" "FARP"
@@ -4328,10 +4365,11 @@ function UTILS.SpawnFARPAndFunctionalStatics(Name,Coordinate,FARPType,Coalition,
newfarp:InitFARP(callsign,freq,mod,DynamicSpawns,HotStart)
local spawnedfarp = newfarp:SpawnFromCoordinate(farplocation,0,Name)
table.insert(ReturnObjects,spawnedfarp)
PopulateStorage(Name,liquids,equip,airframes)
end
PopulateStorage(Name,liquids,equip,airframes)
-- Spawn Objects
local FARPStaticObjectsNato = {
["FUEL"] = { TypeName = "FARP Fuel Depot", ShapeName = "GSM Rus", Category = "Fortifications"},
@@ -5274,3 +5312,147 @@ function UTILS.CreateAirbaseEnum()
_savefile(string.format("%s-enums.txt", env.mission.theatre), text)
--env.info(text)
end
--- Calculate then center and radius of a circle enclosing a list if DCS#Vec2 points.
-- @param #table points Table of DCS#Vec2 entries
-- @return DCS#Vec2 center DCS#Vec2
-- @return #number radius
function UTILS.GetCenterAndRadius(points)
if #points == 0 then
return nil, nil
end
-- Calculate centroid (average of all points)
local sumX, sumY = 0, 0
for _, p in ipairs(points) do
sumX = sumX + p.x
sumY = sumY + p.y
end
local center = {
x = sumX / #points,
y = sumY / #points
}
-- Find maximum distance from center to any point
local maxDist = 0
for _, p in ipairs(points) do
local dx = p.x - center.x
local dy = p.y - center.y
local dist = math.sqrt(dx*dx + dy*dy)
if dist > maxDist then
maxDist = dist
end
end
return center, maxDist
end
--- More accurate: Minimum bounding circle (Welzl's algorithm), calculate then center and radius of a circle enclosing a list if DCS#Vec2 points.
-- @param #table points Table of DCS#Vec2 entries
-- @return DCS#Vec2 center DCS#Vec2
-- @return #number radius
function UTILS.GetMinimumBoundingCircle(points)
if #points == 0 then
return nil, nil
end
-- Calculate distance between two points
local function distance(p1, p2)
local dx = p2.x - p1.x
local dy = p2.y - p1.y
return math.sqrt(dx*dx + dy*dy)
end
-- Circle from 2 points (diameter)
local function circleFrom2Points(p1, p2)
local center = {
x = (p1.x + p2.x) / 2,
y = (p1.y + p2.y) / 2
}
local radius = distance(p1, p2) / 2
return center, radius
end
-- Circle from 3 points (circumcircle)
local function circleFrom3Points(p1, p2, p3)
local ax, ay = p1.x, p1.y
local bx, by = p2.x, p2.y
local cx, cy = p3.x, p3.y
local d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
if math.abs(d) < 0.0001 then
-- Points are collinear, use 2-point circle
return circleFrom2Points(p1, p3)
end
local aSq = ax*ax + ay*ay
local bSq = bx*bx + by*by
local cSq = cx*cx + cy*cy
local ux = (aSq * (by - cy) + bSq * (cy - ay) + cSq * (ay - by)) / d
local uy = (aSq * (cx - bx) + bSq * (ax - cx) + cSq * (bx - ax)) / d
local center = {x = ux, y = uy}
local radius = distance(center, p1)
return center, radius
end
-- Check if point is inside circle
local function isInside(center, radius, point, tolerance)
tolerance = tolerance or 0.0001
return distance(center, point) <= radius + tolerance
end
-- Welzl's algorithm (recursive)
local function welzlHelper(pts, n, boundary)
-- Base cases
if n == 0 or #boundary == 3 then
if #boundary == 0 then
return {x = 0, y = 0}, 0
elseif #boundary == 1 then
return {x = boundary[1].x, y = boundary[1].y}, 0
elseif #boundary == 2 then
return circleFrom2Points(boundary[1], boundary[2])
else
return circleFrom3Points(boundary[1], boundary[2], boundary[3])
end
end
-- Pick a random point
local p = pts[n]
-- Get circle without this point
local center, radius = welzlHelper(pts, n - 1, boundary)
-- If point is inside, we're done
if isInside(center, radius, p) then
return center, radius
end
-- Otherwise, point must be on the boundary
local newBoundary = {}
for i = 1, #boundary do
newBoundary[i] = boundary[i]
end
table.insert(newBoundary, p)
return welzlHelper(pts, n - 1, newBoundary)
end
-- Shuffle points for better average performance
local pts = {}
for i, p in ipairs(points) do
pts[i] = {x = p.x, y = p.y}
end
-- Simple shuffle
for i = #pts, 2, -1 do
local j = math.random(1, i)
pts[i], pts[j] = pts[j], pts[i]
end
return welzlHelper(pts, #pts, {})
end
+52 -1
View File
@@ -27,6 +27,7 @@
-- @field #table parkingByID Parking spot data table with ID as key.
-- @field #table parkingWhitelist List of parking spot terminal IDs considered for spawning.
-- @field #table parkingBlacklist List of parking spot terminal IDs **not** considered for spawning.
-- @field Core.Zone#ZONE_RADIUS parkingCircle Minimum bounding circle enclosing all parking spots.
-- @field #table runways Runways of airdromes.
-- @field #AIRBASE.Runway runwayLanding Runway used for landing.
-- @field #AIRBASE.Runway runwayTakeoff Runway used for takeoff.
@@ -1552,7 +1553,7 @@ AIRBASE.SpotStatus = {
--- Runway data.
-- @type AIRBASE.Runway
-- @field #string name Runway name.
-- @field #string idx Runway ID: heading 070° ==> idx="07".
-- @field #string idx Runway ID: heading 070° ==> idx="07". Mostly same as magheading.
-- @field #number heading True heading of the runway in degrees.
-- @field #number magheading Magnetic heading of the runway in degrees. This is what is marked on the runway.
-- @field #number length Length of runway in meters.
@@ -1662,6 +1663,8 @@ end
else
self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName))
end
self:GetMinimumBoundingCircleFromParkingSpots( )
-- Debug info.
self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName)))
@@ -2191,6 +2194,54 @@ function AIRBASE:GetParkingSpotsCoordinates(termtype)
return spots
end
--- Get the DCS#Vec2s of all parking spots at an airbase. Optionally only those of a specific terminal type. Spots on runways are excluded if not explicitly requested by terminal type.
-- @param #AIRBASE self
-- @param #AIRBASE.TerminalType termtype (Optional) Terminal type. Default all.
-- @return #table Table of DCS#Vec2 of parking spots.
function AIRBASE:GetParkingSpotsVec2s(termtype)
-- Get all parking spots data.
local parkingdata=self:GetParkingData(false)
-- Put coordinates of free spots into table.
local spots={}
for _,parkingspot in ipairs(parkingdata) do
-- Coordinates on runway are not returned unless explicitly requested.
if AIRBASE._CheckTerminalType(parkingspot.Term_Type, termtype) then
-- Get coordinate from Vec3 terminal position.
local vec2 = { x = parkingspot.vTerminalPos.x, y = parkingspot.vTerminalPos.z }
-- Add to table.
table.insert(spots, vec2)
end
end
return spots
end
--- Get the the bounding circular zone around all parking spots of an airbase.
-- @param #AIRBASE self
-- @param #boolean mark (Optional) Draw zone on map on first call of this function.
-- @return Core.Zone#ZONE_RADIUS BoundingZone
function AIRBASE:GetMinimumBoundingCircleFromParkingSpots(mark)
if self.isAirdrome then
if not self.parkingCircle then
local spots = self:GetParkingSpotsVec2s()
local center, radius = UTILS.GetMinimumBoundingCircle(spots)
self.parkingCircle = ZONE_RADIUS:New(self.AirbaseName.." ParkingCircle",center,radius+50)
if mark == true then
self.parkingCircle:DrawZone(-1,{1,0,0},1,{0,1,0},0.2,3)
end
end
return self.parkingCircle
else
return self.AirbaseZone
end
end
--- Get a table containing the coordinates, terminal index and terminal type of free parking spots at an airbase.
-- @param #AIRBASE self
-- @return#AIRBASE self
+13 -3
View File
@@ -2439,7 +2439,7 @@ end
-- @return #table The mission route defined by points.
function GROUP:GetTaskRoute()
--self:F2( self.GroupName )
if _DATABASE.Templates.Groups[self.GroupName].Template and _DATABASE.Templates.Groups[self.GroupName].Template.route and _DATABASE.Templates.Groups[self.GroupName].Template.route.points then
if _DATABASE.Templates.Groups[self.GroupName] and _DATABASE.Templates.Groups[self.GroupName].Template and _DATABASE.Templates.Groups[self.GroupName].Template.route and _DATABASE.Templates.Groups[self.GroupName].Template.route.points then
return UTILS.DeepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points )
else
return {}
@@ -2467,7 +2467,7 @@ function GROUP:CopyRoute( Begin, End, Randomize, Radius )
--self:T3( { GroupName } )
local Template = _DATABASE.Templates.Groups[GroupName].Template
local Template = _DATABASE.Templates.Groups[GroupName] and _DATABASE.Templates.Groups[GroupName].Template or nil
if Template then
if not Begin then
@@ -2491,7 +2491,7 @@ function GROUP:CopyRoute( Begin, End, Randomize, Radius )
end
return Points
else
error( "Template not found for Group : " .. GroupName )
BASE:E( "Template not found for Group : " .. GroupName )
end
return nil
@@ -2944,6 +2944,16 @@ function GROUP:EnableEmission(switch)
return self
end
--- Set a formation for this group.
-- @param #GROUP self
-- @param #number Formation See. ENUMS.Formation or [Formations](https://wiki.hoggitworld.com/view/DCS_enum_formation) for options.
-- @return #GROUP self
function GROUP:SetFormation(Formation)
self:SetOption(AI.Option.Air.id.FORMATION,Formation)
return self
end
--- Switch on/off invisible flag for the group.
-- @param #GROUP self
-- @param #boolean switch If true, Invisible is enabled. If false, Invisible is disabled.
+1 -1
View File
@@ -485,7 +485,7 @@ function NET:GetPlayerIDByName(Name)
if not Name then return nil end
local playerList = net.get_player_list()
for i=1,#playerList do
local playerName = net.get_name(i)
local playerName = net.get_name(playerList[i])
if playerName == Name then
return playerList[i]
end
@@ -729,16 +729,23 @@ end
--- Returns the altitude above sea level of the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level.
-- @return DCS#Distance The altitude of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.
function POSITIONABLE:GetAltitude()
function POSITIONABLE:GetAltitude(FromGround)
self:F2()
local DCSPositionable = self:GetDCSObject()
if DCSPositionable then
local PositionablePointVec3 = DCSPositionable:getPoint() -- DCS#Vec3
return PositionablePointVec3.y
local altitude = 0
local point = DCSPositionable:getPoint() --DCS#Vec3
altitude = point.y
if FromGround then
local land = land.getHeight({ x = point.x, y = point.z }) or 0
altitude = altitude - land
end
return altitude
end
BASE:E( { "Cannot GetAltitude", Positionable = self, Alive = self:IsAlive() } )
+1 -1
View File
@@ -75,7 +75,7 @@ function SCENERY:Register( SceneryName, SceneryObject, SceneryZone )
self.Vector = (self.Vec3 and VECTOR) and VECTOR:NewFromVec(self.Vec3) or nil
end
if SceneryObject then
if SceneryObject and SceneryObject.getPoint then
local vec3 = SceneryObject:getPoint()
self.Vec3 = { x = vec3.x, y = vec3.y, z = vec3.z }
self.Vec2 = { x = vec3.x, y = vec3.z }
+1
View File
@@ -104,6 +104,7 @@ Ops/FlightControl.lua
Ops/PlayerTask.lua
Ops/PlayerRecce.lua
Ops/EasyGCICAP.lua
Ops/EasyA2G.lua
Ops/OpsZone.lua
Ops/ArmyGroup.lua
Ops/OpsTransport.lua