Compare commits

...

140 Commits

Author SHA1 Message Date
Thomas c335881b2d Merge pull request #2505 from shaji-Dev/master
[ADDED] 268 Missing Warehouse EQP ENUMS
2026-02-09 18:09:24 +01:00
smiki 993fc45e83 [ADDED] 268 Missing Warehouse EQP ENUMS 2026-02-09 16:48:32 +01:00
Thomas 130d358f4a Merge pull request #2499 from shaji-Dev/master
[FIXED] nil function
2026-02-06 12:37:28 +01:00
shaji-Dev 49476abb9f Merge branch 'FlightControl-Master:master' into master 2026-02-06 11:27:21 +01:00
smiki a02b99036b [FIXED] nil function 2026-02-06 11:26:57 +01:00
Applevangelist dab6f48fe9 xx 2026-02-05 12:20:48 +01:00
Thomas 23c9ea0961 Merge pull request #2497 from shaji-Dev/master
Optimizations
2026-02-05 12:18:41 +01:00
smiki d0b04ebbbd Optimizations 2026-02-05 10:52:44 +01:00
smiki bd64d084e9 Optimizations 2026-02-05 10:49:48 +01:00
Thomas c91b59f8cd Merge pull request #2495 from shaji-Dev/master
COMMANDER and CHIEF fixes and additions
2026-01-30 06:39:41 +01:00
smiki a699300f5f [ADDED] COMMANDER:CanMission(Mission) and CHIEF:CanMission(Mission) 2026-01-29 21:43:27 +01:00
smiki 6e12c5c1d0 [FIX] Added Alias to COMMANDER init from CHIEF 2026-01-29 18:34:37 +01:00
smiki 852fc1d2d5 Merge branch 'master' of https://github.com/shaji-Dev/MOOSE
# Conflicts:
#	Moose Development/Moose/Core/Event.lua
2026-01-29 18:33:35 +01:00
smiki c2c957b859 [FIX] Added Alias to COMMANDER init from CHIEF 2026-01-29 18:32:04 +01:00
Applevangelist f20c22ab49 #EVENT - Save some memory by avoiding to register SCENERY yet unregistered and avoiding creating COORDINATEs on Marks (still has a Vec3) when not switched on 2026-01-28 14:41:51 +01:00
smiki 1e200511cb [FIX] Memory leak caused by continuous F10 redrawing that for marker (add, remove) which creates new coordinate. 2026-01-28 12:06:11 +01:00
smiki 80f2a0ef39 [FIX] Memory leak caused by continuous F10 redrawing that for marker (add, remove) which creates new coordinate. 2026-01-28 12:05:32 +01:00
smiki ae156259c0 [FIX] Memory leak caused by players bombing neighborhoods and creating hundreds of Dead events for scenery that registers their objects. 2026-01-28 12:01:53 +01:00
smiki 39e3f8b646 [FIX] Memory leak caused by continuous F10 redrawing that for marker (add, remove) which creates new coordinate. 2026-01-28 11:56:09 +01:00
Applevangelist ac61c3a55c #MANTIS - Added more flags for systems able to shoot down incoming missiles
#SEAD/#SHORAD/#MANTIS - Fixed logic and governance problems with SHORAD engagement
2026-01-25 13:14:53 +01:00
Thomas 1054ffa7ac Merge pull request #2492 from leka1986/patch-8
Update CTLD.lua
2026-01-24 18:46:17 +01:00
leka1986 41503ae625 Update CTLD.lua
fixed menu getting deleted.
2026-01-24 18:00:25 +01:00
Applevangelist 1ee90e67b2 #UTILS - LoadStationaryListOfGroups() added option to (randomly) restore dead units on reload. 2026-01-24 16:21:37 +01:00
Applevangelist d0ce7f1aa6 xx 2026-01-24 16:16:50 +01:00
Thomas c387749a81 Merge pull request #2490 from leka1986/patch-7
Fixed bugs
2026-01-23 19:21:22 +01:00
Applevangelist b980c5d93a #EasyA2G/GCICAP - Added option to set Squadron Turnovertime 2026-01-23 18:20:24 +01:00
leka1986 49332fafec Fixed bugs
Fixed bugs for load troops.

Added Mi-8 to the prevention method for ignoring what is inside when trying to build crates.

Removed return self if the player is trying to build stuff that is outside the helicopter or C-130, while he still have stuff inside. Cargo will be built but it will ignore what is inside.

Removed the menu "Others" if there is no sub category involved for that type. If user have crates subcat but not troops subcat, it won't show "others".
2026-01-22 18:45:23 +01:00
Applevangelist 3ab3278db4 xx 2026-01-22 17:18:59 +01:00
Applevangelist 7a3ea2918d #SEAD/#MANTIS - Adding functions for non-suppression of groups capable of HARM defense
#CTLD/#CSAR - Added data for A/M-H6J
2026-01-22 12:35:16 +01:00
Thomas 9c0394f035 Update Utils.lua
#UTILS - Added  `UTILS.Vec3toVec2(Vec3)`
2026-01-20 11:38:34 +01:00
Applevangelist 9536d368b0 xx 2026-01-12 13:13:48 +01:00
Applevangelist 9426905959 #AIRBASE - Fix parking data table could be nil 2026-01-12 08:48:37 +01:00
Applevangelist 99b7b4b057 #EASYGCICAP - Fix error in setting floor/ceiling for corridors 2026-01-09 15:38:05 +01:00
Applevangelist 2fdbdb4058 #RECOVERYTANKER - Ensure correct "F10 Name" for A6E if used as tanker 2026-01-08 18:51:13 +01:00
Applevangelist 898405e15f Merge remote-tracking branch 'origin/master' 2026-01-08 13:11:34 +01:00
Applevangelist 7259038b1c xx 2026-01-08 13:10:48 +01:00
Applevangelist 3886351490 xx 2026-01-08 13:10:37 +01:00
Thomas 3aedf8e5d0 Merge pull request #2487 from leka1986/patch-14
Update CTLD.lua
2026-01-05 06:58:07 +01:00
leka1986 aed2f1d8a9 Enhance troop loading logic and messaging
noticed a bug, I could load troops outside the zone when choosing amount. I could also load without the door was even checked.

Combined the troop drop to one message instead of multiple messages for each dropped group.
2026-01-05 04:18:57 +01:00
leka1986 3504797147 Update CTLD.lua
Fixed so the Engineer can remove all the items when he is building from an airdrop.

Added a function that I use, CanGetCrates, it's a check before getting it if the conditions are met.

Added small fixes where calling FindCratesNearBy would remove the units when it's called by the function RemoveCratesNearBy when it was checking the surroundings and it should ignore what is inside. In the case of C-130 and the CH-47.

Added how many crates are dropped when getting statics.

Expanded OnAfterTroopsRTB to accept chunk, ie the number of units which is returned.
2026-01-05 03:43:28 +01:00
Applevangelist 3cfaa60ebb #VARIOUS fixes 2026-01-04 17:52:55 +01:00
Applevangelist 2d930ff2dd #SCORING - missed data for saving in format() 2026-01-03 17:26:17 +01:00
Frank 11a8792398 Update Warehouse.lua
- fixed bug that assets cannot be found in DB and therefore are not added to the requesting warehouse
2025-12-31 13:17:13 +01:00
Applevangelist f7556fac0e #UNIT - Added GetFuelMassMax() and GetCurrentFileKgs() 2025-12-30 10:40:27 +01:00
Applevangelist 394f3ffd88 #AIRBASE - Small fix for #ParkingSpots == 0 2025-12-30 09:26:07 +01:00
Applevangelist a3d7b4eba9 #AIRWING - Check if STORAGE object has NO warehouse! 2025-12-29 18:08:53 +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 d000ad76f8 #CSAR - Corrected Airbase dist finding solution 2025-12-29 10:48:23 +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 1eb6dbc7f2 xx 2025-12-28 16:51:41 +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 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 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 8c0e021e82 #CTLD - Make FSM event CratesDropped useable for tasking 2025-12-23 17:16:57 +01:00
Applevangelist a72d3afd1c xx 2025-12-22 14:09:37 +01:00
Applevangelist 68aab10902 xx 2025-12-22 12:21:42 +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 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 a5eef3087b #CSAR - added option for Smoke Closest MASH, adjusted logic for airbase rescues 2025-12-21 12:49:43 +01:00
Applevangelist af891f3fc2 #2479 Added S-300VM/S-300V4/S-400 SAM data 2025-12-21 11:43:29 +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 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 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 87b87fa2ca Update Database.lua 2025-12-17 19:34:06 +01:00
Applevangelist a39f39646d #MARKEROPS - Put case sensitivity switching into own functions 2025-12-17 11:31:55 +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 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 33ff723bd8 Further refinement for acoustic detection and smoke 2025-12-15 12:13:04 +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 b6a90d4633 #GROUP Added GROUP:SetFormation(Formation) 2025-12-14 16:05:18 +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 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
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
43 changed files with 5156 additions and 828 deletions
+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: |
+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
+19 -37
View File
@@ -184,6 +184,7 @@ EVENT = {
ClassName = "EVENT",
ClassID = 0,
MissionEnd = false,
CreateMarkCoordinateOnEvent = false,
}
world.event.S_EVENT_NEW_CARGO = world.event.S_EVENT_MAX + 1000
@@ -339,14 +340,18 @@ EVENTS = {
-- @field #string WeaponName Name of the weapon.
-- @field DCS#Unit WeaponTgtDCSUnit Target DCS unit of the weapon.
--
-- @field Cargo.Cargo#CARGO Cargo The cargo object.
-- @field #string CargoName The name of the cargo object.
--
-- @field Core.Zone#ZONE Zone The zone object.
-- @field #string ZoneName The name of the zone.
--
-- @field Wrapper.DynamicCargo#DYNAMICCARGO IniDynamicCargo The dynamic cargo object.
-- @field #string IniDynamicCargoName The dynamic cargo unit name.
--
-- @field #number MarkCoalition Coalition of a marker (if any)
-- @field DCS#Vec3 MarkVec3 Position of a marker
-- @field #string MarkText Text content of a marker, if any
-- @field #number MarkID ID of the marker, for deletion
-- @field #number MarkGroupID Group ID of the group that created the marker
-- @field Core.Point#COORDINATE Coordinate object of the marker, only filled if EVENT.CreateMarkCoordinateOnEvent is set to true (off by default)
@@ -1063,36 +1068,6 @@ end
do -- Event Creation
--- Creation of a New Cargo Event.
-- @param #EVENT self
-- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created.
function EVENT:CreateEventNewCargo( Cargo )
self:F( { Cargo } )
local Event = {
id = EVENTS.NewCargo,
time = timer.getTime(),
cargo = Cargo,
}
world.onEvent( Event )
end
--- Creation of a Cargo Deletion Event.
-- @param #EVENT self
-- @param AI.AI_Cargo#AI_CARGO Cargo The Cargo created.
function EVENT:CreateEventDeleteCargo( Cargo )
self:F( { Cargo } )
local Event = {
id = EVENTS.DeleteCargo,
time = timer.getTime(),
cargo = Cargo,
}
world.onEvent( Event )
end
--- Creation of a New Zone Event.
-- @param #EVENT self
-- @param Core.Zone#ZONE_BASE Zone The Zone created.
@@ -1361,7 +1336,8 @@ function EVENT:onEvent( Event )
Event.IniDynamicCargoName = Event.IniUnitName
Event.IniPlayerName = string.match(Event.IniUnitName,"^(.+)|%d%d:%d%d|PKG%d+")
else
Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName )
--Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName )
Event.IniUnit = STATIC:FindByName( Event.IniDCSUnitName, false )
end
Event.IniCoalition = Event.IniDCSUnit:getCoalition()
Event.IniCategory = Event.IniDCSUnit:getDesc().category
@@ -1374,7 +1350,9 @@ function EVENT:onEvent( Event )
Event.IniDCSUnit = Event.initiator
Event.IniDCSUnitName = ( Event.IniDCSUnit and Event.IniDCSUnit.getName ) and Event.IniDCSUnit:getName() or "Scenery no name "..math.random(1,20000)
Event.IniUnitName = Event.IniDCSUnitName
Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator )
local ID = (Event.IniDCSUnit and Event.IniDCSUnit.getID) and Event.IniDCSUnit:getID() or Event.IniDCSUnitName
Event.IniUnit = (_SCENERY ~= nil) and _SCENERY[ID] or nil
--Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator )
Event.IniCategory = (Event.IniDCSUnit and Event.IniDCSUnit.getDesc ) and Event.IniDCSUnit:getDesc().category
Event.IniTypeName = (Event.initiator and Event.initiator.isExist
and Event.initiator:isExist() and Event.IniDCSUnit and Event.IniDCSUnit.getTypeName) and Event.IniDCSUnit:getTypeName() or "SCENERY"
@@ -1478,7 +1456,9 @@ function EVENT:onEvent( Event )
Event.TgtDCSUnitName = Event.TgtDCSUnit.getName and Event.TgtDCSUnit:getName() or nil
if Event.TgtDCSUnitName~=nil then
Event.TgtUnitName = Event.TgtDCSUnitName
Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target )
local ID = (Event.TgtDCSUnit and Event.TgtDCSUnit.getID) and Event.TgtDCSUnit:getID() or Event.TgtDCSUnitName
--Event.TgtUnit = SCENERY:Register( Event.TgtDCSUnitName, Event.target )
Event.TgtUnit = (_SCENERY ~= nil) and _SCENERY[ID] or nil
Event.TgtCategory = Event.TgtDCSUnit:getDesc().category
Event.TgtTypeName = Event.TgtDCSUnit:getTypeName()
end
@@ -1519,7 +1499,9 @@ function EVENT:onEvent( Event )
if Event.idx then
Event.MarkID=Event.idx
Event.MarkVec3=Event.pos
Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos)
if self.CreateMarkCoordinateOnEvent == true then
Event.MarkCoordinate=COORDINATE:NewFromVec3(Event.pos)
end
Event.MarkText=Event.text
Event.MarkCoalition=Event.coalition
Event.IniCoalition=Event.coalition
+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.
--------------------------------------------------------------------------
+14 -1
View File
@@ -3281,7 +3281,6 @@ do -- COORDINATE
return delta/60
end
--- Return a BR string from a COORDINATE to the COORDINATE.
-- @param #COORDINATE self
-- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from.
@@ -3295,6 +3294,20 @@ do -- COORDINATE
local Distance = self:Get2DDistance( FromCoordinate )
return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings, nil, MagVar, Precision )
end
--- Return a Bearing string from a COORDINATE to the (self) COORDINATE.
-- @param #COORDINATE self
-- @param #COORDINATE FromCoordinate The coordinate to measure the distance and the bearing from.
-- @param Core.Settings#SETTINGS Settings (optional) The settings. Can be nil, and in this case the default settings are used. If you want to specify your own settings, use the _SETTINGS object.
-- @param #boolean MagVar If true, also get angle in MagVar for BR/BRA
-- @param #number Precision Rounding precision, currently full km as default (=0)
-- @return #string The BR text.
function COORDINATE:ToStringBearing( FromCoordinate, Settings, MagVar, Precision )
local DirectionVec3 = FromCoordinate:GetDirectionVec3( self )
local AngleRadians = self:GetAngleRadians( DirectionVec3 )
--local Distance = self:Get2DDistance( FromCoordinate )
return self:GetBearingText(AngleRadians,Precision,Settings,MagVar)
end
--- Return a BRA string from a COORDINATE to the COORDINATE.
-- @param #COORDINATE self
-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)
+12 -13
View File
@@ -693,7 +693,7 @@ end
function AUTOLASE:GetLosFromUnit(Unit)
local lasedistance = self.LaseDistance
local unitheight = Unit:GetHeight()
local coord = Unit:GetCoordinate()
local coord = Unit:GetCoord()
local landheight = coord:GetLandHeight()
local asl = unitheight - landheight
if asl > 100 then
@@ -845,7 +845,7 @@ function AUTOLASE:ShowStatus(Group,Unit)
locationstring = entry.coordinate:ToStringLLDDM(settings)
elseif settings:IsA2G_BR() then
-- attention this is the distance from the ASKING unit to target, not from RECCE to target!
local startcoordinate = Unit:GetCoordinate() or Group:GetCoordinate()
local startcoordinate = Unit:GetCoord() or Group:GetCoord()
locationstring = entry.coordinate:ToStringBR(startcoordinate,settings,false,self.RoundingPrecision)
end
end
@@ -963,8 +963,8 @@ function AUTOLASE:CanLase(Recce,Unit)
end
end
-- calculate LOS
local reccecoord = Recce:GetCoordinate()
local unitcoord = Unit:GetCoordinate()
local reccecoord = Recce:GetCoord()
local unitcoord = Unit:GetCoord()
local islos = reccecoord:IsLOS(unitcoord,2.5)
-- calculate distance
local distance = math.floor(reccecoord:Get3DDistance(unitcoord))
@@ -1020,8 +1020,8 @@ function AUTOLASE:_Prescient()
self:T(self.lid.."Checking possibly visible STATICs for Recce "..unit:GetName())
for _,_static in pairs(Statics) do -- DCS static object here
local static = STATIC:Find(_static)
if static and static:GetCoalition() ~= self.coalition and static:GetCoordinate() then
local IsLOS = position:IsLOS(static:GetCoordinate())
if static and static:GetCoalition() ~= self.coalition and static:GetCoord() then
local IsLOS = position:IsLOS(static:GetCoord())
if IsLOS then
unit:KnowUnit(static,true,true)
end
@@ -1081,7 +1081,7 @@ function AUTOLASE:onafterMonitor(From, Event, To)
local threat = contact.threatlevel or 0
local reccegrp = UNIT:FindByName(reccename)
if reccegrp then
local reccecoord = reccegrp:GetCoordinate()
local reccecoord = reccegrp:GetCoord()
local distance = math.floor(reccecoord:Get3DDistance(coord))
local text = string.format("%s of %s | Distance %d km | Threatlevel %d",contact.attribute, contact.groupname, math.floor(distance/1000), contact.threatlevel)
report:Add(text)
@@ -1120,7 +1120,6 @@ function AUTOLASE:onafterMonitor(From, Event, To)
local unit = _unit -- Wrapper.Unit#UNIT
if unit and unit:IsAlive() then
local threat = unit:GetThreatLevel()
local coord = unit:GetCoordinate()
if threat >= self.minthreatlevel then
local unitname = unit:GetName()
-- prefer radar units
@@ -1176,16 +1175,16 @@ function AUTOLASE:onafterMonitor(From, Event, To)
local code = self:GetLaserCode(reccename)
local spot = SPOT:New(recce)
spot:LaseOn(unit,code,self.LaseDuration)
local locationstring = unit:GetCoordinate():ToStringLLDDM()
local locationstring = unit:GetCoord():ToStringLLDDM()
if _SETTINGS:IsA2G_MGRS() then
local precision = _SETTINGS:GetMGRS_Accuracy()
local settings = {}
settings.MGRS_Accuracy = precision
locationstring = unit:GetCoordinate():ToStringMGRS(settings)
locationstring = unit:GetCoord():ToStringMGRS(settings)
elseif _SETTINGS:IsA2G_LL_DMS() then
locationstring = unit:GetCoordinate():ToStringLLDMS(_SETTINGS)
locationstring = unit:GetCoord():ToStringLLDMS(_SETTINGS)
elseif _SETTINGS:IsA2G_BR() then
locationstring = unit:GetCoordinate():ToStringBULLS(self.coalition,_SETTINGS)
locationstring = unit:GetCoord():ToStringBULLS(self.coalition,_SETTINGS)
end
local laserspot = { -- #AUTOLASE.LaserSpot
@@ -1198,7 +1197,7 @@ function AUTOLASE:onafterMonitor(From, Event, To)
unitname = unitname,
reccename = reccename,
unittype = unit:GetTypeName(),
coordinate = unit:GetCoordinate(),
coordinate = unit:GetCoord(),
}
if self.smoketargets then
local coord = unit:GetCoordinate()
+519 -85
View File
@@ -22,7 +22,7 @@
-- @module Functional.Mantis
-- @image Functional.Mantis.jpg
--
-- Last Update: August 2025
-- Last Update: January 2026
-------------------------------------------------------------------------
--- **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.44",
SAM_Templates_Prefix = "",
SAM_Group = nil,
EWR_Templates_Prefix = "",
@@ -331,6 +334,11 @@ MANTIS = {
checkcounter = 1,
DLinkCacheTime = 120,
logsamstatus = false,
DetectAccoustic = false,
DetectAccousticRadius = 2000,
DetectAccousticCategories = {Unit.Category.HELICOPTER},
ARMWeaponSeen = {},
InboundARMs = {},
}
--- Advanced state enumerator
@@ -366,24 +374,25 @@ MANTIS.radiusscale[MANTIS.SamType.POINT] = 3
-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range)
-- @field #string Radar Radar typename on unit level (used as key)
-- @field #string Point Point defense capable
-- @field ARMCapacit ARMCapacity ie how many (H)ARMs the system can defend at the same time
MANTIS.SamData = {
["Hawk"] = { Range=35, Blindspot=0, Height=12, Type="Medium", Radar="Hawk" }, -- measures in km
["NASAMS"] = { Range=14, Blindspot=0, Height=7, Type="Short", Radar="NSAMS" }, -- AIM 120B
["NASAMS"] = { Range=14, Blindspot=0, Height=7, Type="Short", Radar="NSAMS", ARMCapacity=1 }, -- AIM 120B
["Patriot"] = { Range=99, Blindspot=0, Height=25, Type="Long", Radar="Patriot str" },
["Rapier"] = { Range=10, Blindspot=0, Height=3, Type="Short", Radar="rapier" },
["SA-2"] = { Range=40, Blindspot=7, Height=25, Type="Medium", Radar="S_75M_Volhov" },
["SA-3"] = { Range=18, Blindspot=6, Height=18, Type="Short", Radar="5p73 s-125 ln" },
["SA-5"] = { Range=250, Blindspot=7, Height=40, Type="Long", Radar="5N62V" },
["SA-6"] = { Range=25, Blindspot=0, Height=8, Type="Medium", Radar="1S91" },
["SA-10"] = { Range=119, Blindspot=0, Height=18, Type="Long" , Radar="S-300PS 4"},
["SA-10"] = { Range=119, Blindspot=0, Height=18, Type="Long" , Radar="S-300PS 4", ARMCapacity=4},
["SA-11"] = { Range=35, Blindspot=0, Height=20, Type="Medium", Radar="SA-11" },
["Roland"] = { Range=6, Blindspot=0, Height=5, Type="Short", Radar="Roland" },
["Roland"] = { Range=6, Blindspot=0, Height=5, Type="Short", Radar="Roland", ARMCapacity=1 },
["Gepard"] = { Range=5, Blindspot=0, Height=4, Type="Point", Radar="Gepard" },
["HQ-7"] = { Range=12, Blindspot=0, Height=3, Type="Short", Radar="HQ-7" },
["SA-9"] = { Range=4, Blindspot=0, Height=3, Type="Point", Radar="Strela", Point="true" },
["SA-8"] = { Range=10, Blindspot=0, Height=5, Type="Short", Radar="Osa 9A33" },
["SA-19"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Tunguska" },
["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Point", Radar="Tor 9A331", Point="true" },
["SA-15"] = { Range=11, Blindspot=0, Height=6, Type="Point", Radar="Tor 9A331", Point="true", ARMCapacity=2 },
["SA-13"] = { Range=5, Blindspot=0, Height=3, Type="Point", Radar="Strela", Point="true" },
["Avenger"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="Avenger" },
["Chaparral"] = { Range=8, Blindspot=0, Height=3, Type="Short", Radar="Chaparral" },
@@ -391,19 +400,23 @@ MANTIS.SamData = {
["Silkworm"] = { Range=90, Blindspot=1, Height=0.2, Type="Long", Radar="Silkworm" },
["C-RAM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="HEMTT_C-RAM_Phalanx", Point="true" },
-- units from HDS Mod, multi launcher options is tricky
["SA-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B"},
["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-10B"] = { Range=75, Blindspot=0, Height=18, Type="Medium" , Radar="SA-10B", ARMCapacity=4},
["SA-17"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17", ARMCapacity=4 },
["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1", ARMCapacity=16},
["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2", ARMCapacity=18},
["SA-21"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="92N6E"},
["S-300VM"] = { Range=200, Blindspot=5, Height=30, Type="Long" , Radar="9S32M", ARMCapacity=4},
["S-300V4"] = { Range=380, Blindspot=5, Height=30, Type="Long" , Radar="9S32M", ARMCapacity=4},
["S-400"] = { Range=250, Blindspot=5, Height=27, Type="Long" , Radar="92N6E", ARMCapacity=4},
["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" },
["Tor M2"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true" },
["IRIS-T SLM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM" },
["Pantsir S1"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1" , Point="true", ARMCapacity=3 },
["Tor M2"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true", ARMCapacity=4 },
["IRIS-T SLM"] = { Range=40, Blindspot=0.5, Height=20, Type="Medium", Radar="CH_IRIST_SLM", ARMCapacity=12 }, -- 4 per starter, usually 3 starters in a battery
}
--- SAM data HDS
@@ -419,13 +432,13 @@ MANTIS.SamDataHDS = {
-- group name MUST contain HDS to ID launcher type correctly!
["SA-2 HDS"] = { Range=56, Blindspot=7, Height=30, Type="Medium", Radar="V759" },
["SA-3 HDS"] = { Range=20, Blindspot=6, Height=30, Type="Short", Radar="V-601P" },
["SA-10B HDS"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85CE ln"}, -- V55RUD
["SA-10C HDS"] = { Range=75, Blindspot=5, Height=25, Type="Long" , Radar="5P85SE ln"}, -- V55RUD
["SA-17 HDS"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17 " },
["SA-12 HDS 2"] = { Range=100, Blindspot=13, Height=30, Type="Long" , Radar="S-300V 9A82 l"},
["SA-12 HDS 1"] = { Range=75, Blindspot=6, Height=25, Type="Long" , Radar="S-300V 9A83 l"},
["SA-23 HDS 2"] = { Range=200, Blindspot=5, Height=37, Type="Long", Radar="S-300VM 9A82ME" },
["SA-23 HDS 1"] = { Range=100, Blindspot=1, Height=50, Type="Long", Radar="S-300VM 9A83ME" },
["SA-10B HDS"] = { Range=90, Blindspot=5, Height=25, Type="Long" , Radar="5P85CE ln", ARMCapacity=8}, -- V55RUD
["SA-10C HDS"] = { Range=75, Blindspot=5, Height=25, Type="Long" , Radar="5P85SE ln", ARMCapacity=3}, -- V55RUD
["SA-17 HDS"] = { Range=50, Blindspot=3, Height=50, Type="Medium", Radar="SA-17", ARMCapacity=4 },
["SA-12 HDS 2"] = { Range=100, Blindspot=13, Height=30, Type="Long" , Radar="S-300V 9A82 l", ARMCapacity=12},
["SA-12 HDS 1"] = { Range=75, Blindspot=6, Height=25, Type="Long" , Radar="S-300V 9A83 l", ARMCapacity=12},
["SA-23 HDS 2"] = { Range=200, Blindspot=5, Height=37, Type="Long", Radar="S-300VM 9A82ME", ARMCapacity=14 },
["SA-23 HDS 1"] = { Range=100, Blindspot=1, Height=50, Type="Long", Radar="S-300VM 9A83ME", ARMCapacity=14 },
["HQ-2 HDS"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" },
["SAMPT Block 1 HDS"] = { Range=120, Blindspot=1, Height=20, Type="long", Radar="SAMPT_MLT_Blk1" }, -- Block 1 Launcher
["SAMPT Block 1INT HDS"] = { Range=150, Blindspot=1, Height=25, Type="long", Radar="SAMPT_MLT_Blk1NT" }, -- Block 1-INT Launcher
@@ -469,21 +482,21 @@ MANTIS.SamDataCH = {
-- https://www.currenthill.com/
-- group name MUST contain CHM to ID launcher type correctly!
["2S38 CHM"] = { Range=6, Blindspot=0.1, Height=4.5, Type="Short", Radar="2S38" },
["PantsirS1 CHM"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1", Point="true" },
["PantsirS2 CHM"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2" },
["PantsirS1 CHM"] = { Range=20, Blindspot=1.2, Height=15, Type="Point", Radar="PantsirS1", Point="true", ARMCapacity=3 },
["PantsirS2 CHM"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2", ARMCapacity=4 },
["PGL-625 CHM"] = { Range=10, Blindspot=1, Height=5, Type="Short", Radar="PGL_625" },
["HQ-17A CHM"] = { Range=15, Blindspot=1.5, Height=10, Type="Short", Radar="HQ17A" },
["M903PAC2 CHM"] = { Range=120, Blindspot=3, Height=24.5, Type="Long", Radar="MIM104_M903_PAC2" },
["M903PAC3 CHM"] = { Range=160, Blindspot=1, Height=40, Type="Long", Radar="MIM104_M903_PAC3" },
["TorM2 CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true" },
["TorM2K CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2K", Point="true" },
["TorM2M CHM"] = { Range=16, Blindspot=1, Height=10, Type="Point", Radar="TorM2M", Point="true" },
["TorM2 CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2", Point="true", ARMCapacity=3 },
["TorM2K CHM"] = { Range=12, Blindspot=1, Height=10, Type="Point", Radar="TorM2K", Point="true", ARMCapacity=4 },
["TorM2M CHM"] = { Range=16, Blindspot=1, Height=10, Type="Point", Radar="TorM2M", Point="true", ARMCapacity=4 },
["NASAMS3-AMRAAMER CHM"] = { Range=50, Blindspot=2, Height=35.7, Type="Medium", Radar="CH_NASAMS3_LN_AMRAAM_ER" },
["NASAMS3-AIM9X2 CHM"] = { Range=20, Blindspot=0.2, Height=18, Type="Short", Radar="CH_NASAMS3_LN_AIM9X2" },
["C-RAM CHM"] = { Range=2, Blindspot=0, Height=2, Type="Point", Radar="CH_Centurion_C_RAM", Point="true" },
["PGZ-09 CHM"] = { Range=4, Blindspot=0.5, Height=3, Type="Point", Radar="CH_PGZ09", Point="true" },
["S350-9M100 CHM"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="CH_S350_50P6_9M100" },
["S350-9M96D CHM"] = { Range=150, Blindspot=2.5, Height=30, Type="Long", Radar="CH_S350_50P6_9M96D" },
["S350-9M100 CHM"] = { Range=15, Blindspot=1, Height=8, Type="Short", Radar="CH_S350_50P6_9M100", ARMCapacity=20 },
["S350-9M96D CHM"] = { Range=150, Blindspot=2.5, Height=30, Type="Long", Radar="CH_S350_50P6_9M96D", ARMCapacity=20 },
["LAV-AD CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_LAVAD" },
["HQ-22 CHM"] = { Range=170, Blindspot=5, Height=27, Type="Long", Radar="CH_HQ22_LN" },
["PGZ-95 CHM"] = { Range=2.5, Blindspot=0.5, Height=2, Type="Point", Radar="CH_PGZ95",Point="true" },
@@ -495,8 +508,8 @@ MANTIS.SamDataCH = {
["Skynex CHM"] = { Range=3.5, Blindspot=0.1, Height=3.5, Type="Point", Radar="CH_SkynexHX", Point="true" },
["Skyshield CHM"] = { Range=3.5, Blindspot=0.1, Height=3.5, Type="Point", Radar="CH_Skyshield_Gun", Point="true" },
["WieselOzelot CHM"] = { Range=8, Blindspot=0.16, Height=4.8, Type="Short", Radar="CH_Wiesel2Ozelot" },
["BukM3-9M317M CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317M" },
["BukM3-9M317MA CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317MA" },
["BukM3-9M317M CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317M", ARMCapacity=20 },
["BukM3-9M317MA CHM"] = { Range=70, Blindspot=0.25, Height=35, Type="Medium", Radar="CH_BukM3_9A317MA", ARMCapacity=20 },
["SkySabre CHM"] = { Range=30, Blindspot=0.5, Height=10, Type="Medium", Radar="CH_SkySabreLN" },
["Stormer CHM"] = { Range=7.5, Blindspot=0.3, Height=7, Type="Short", Radar="CH_StormerHVM" },
["THAAD CHM"] = { Range=200, Blindspot=40, Height=150, Type="Long", Radar="CH_THAAD_M1120" },
@@ -524,7 +537,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 +561,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 +574,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 = {}
@@ -613,6 +628,8 @@ do
self.autoshorad = true
self.ShoradGroupSet = SET_GROUP:New() -- Core.Set#SET_GROUP
self.FilterZones = Zones
self.LastThreatEval = {}
self.InboundARMs = {}
self.SkateZones = nil
self.SkateNumber = 3
@@ -716,6 +733,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 +845,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 +870,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 +1072,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 +1592,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 +1606,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 +1645,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 +1726,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
@@ -1511,6 +1809,7 @@ do
local group = GROUP:FindByName(grpname) -- Wrapper.Group#GROUP
local units = group:GetUnits()
local SAMData = self.SamData
local ARMCapacity
if mod then
SAMData = self.SamDataHDS
elseif sma then
@@ -1518,22 +1817,23 @@ do
elseif chm then
SAMData = self.SamDataCH
end
--self:I("Looking to auto-match for "..grpname)
self:T("Looking to auto-match for "..grpname)
for _,_unit in pairs(units) do
local unit = _unit -- Wrapper.Unit#UNIT
local type = string.lower(unit:GetTypeName())
--self:I(string.format("Matching typename: %s",type))
local typename = string.lower(unit:GetTypeName())
self:T(string.format("Matching typename: %s",typename))
for idx,entry in pairs(SAMData) do
local _entry = entry -- #MANTIS.SamData
local _radar = string.lower(_entry.Radar)
--self:I(string.format("Trying typename: %s",_radar))
if string.find(type,_radar,1,true) then
self:T(string.format("Trying typename: %s",_radar))
if string.find(typename,_radar,1,true) then
type = _entry.Type
radiusscale = self.radiusscale[type]
range = _entry.Range * 1000 * radiusscale -- max firing range used as switch-on
height = _entry.Height * 1000 -- max firing height
blind = _entry.Blindspot * 100 -- blind spot range
--self:I(string.format("Match: %s - %s",_radar,type))
blind = _entry.Blindspot * 100 -- blind spot range
ARMCapacity = _entry.ARMCapacity
self:T(string.format("Match: %s - %s",_radar,type))
found = true
break
end
@@ -1554,7 +1854,7 @@ do
if not found then
self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname))
end
return range, height, type, blind
return range, height, type, blind, ARMCapacity
end
--- [Internal] Function to get SAM firing data
@@ -1575,6 +1875,7 @@ do
local HDSmod = false
local SMAMod = false
local CHMod = false
local ARMCapacity = 0
if string.find(grpname,"HDS",1,true) then
HDSmod = true
elseif string.find(grpname,"SMA",1,true) then
@@ -1592,6 +1893,7 @@ do
range = _entry.Range * 1000 * radiusscale -- max firing range
height = _entry.Height * 1000 -- max firing height
blind = _entry.Blindspot
ARMCapacity = _entry.ARMCapacity or 0
self:T("Matching Groupname = " .. grpname .. " Range= " .. range)
found = true
break
@@ -1611,14 +1913,14 @@ do
end
--- Tertiary filter if not found
if (not found) or HDSmod or SMAMod or CHMod then
range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod,CHMod)
range, height, type, blind, ARMCapacity = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod,CHMod)
elseif not found then
self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname))
end
if found and string.find(grpname,"SHORAD",1,true) then
type = MANTIS.SamType.POINT -- force short on match
end
return range, height, type, blind
return range, height, type, blind, ARMCapacity
end
--- [Internal] Function to set the SAM start state
@@ -1653,8 +1955,9 @@ do
group:OptionEngageRange(engagerange) --default engagement will be 95% of firing range
local grpname = group:GetName()
local grpcoord = group:GetCoordinate()
local grprange,grpheight,type,blind = self:_GetSAMRange(grpname)
table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type})
local grprange,grpheight,type,blind,ARMCapacity = self:_GetSAMRange(grpname)
if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end
table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity})
--table.insert( SEAD_Grps, grpname )
if type == MANTIS.SamType.LONG then
table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type})
@@ -1718,12 +2021,15 @@ do
if group:IsGround() and group:IsAlive() then
local grpname = group:GetName()
local grpcoord = group:GetCoordinate()
local grprange, grpheight,type,blind = self:_GetSAMRange(grpname)
local grprange, grpheight,type,blind, ARMCapacity = self:_GetSAMRange(grpname)
-- TODO the below might stop working at some point after some hours, needs testing
--local radaralive = group:IsSAM()
if ARMCapacity and ARMCapacity>0 then _group:SetProperty("ARMCapacity",ARMCapacity) end
local radaralive = true
table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type}) -- make the table lighter, as I don't really use the zone here
table.insert( SEAD_Grps, grpname )
table.insert( SAM_Tbl, {grpname, grpcoord, grprange, grpheight, blind, type, ARMCapacity}) -- make the table lighter, as I don't really use the zone here
if type ~= MANTIS.SamType.POINT then
table.insert( SEAD_Grps, grpname )
end
if type == MANTIS.SamType.LONG and radaralive then
table.insert( SAM_Tbl_lg, {grpname, grpcoord, grprange, grpheight, blind, type})
self:T({grpname,grprange, grpheight})
@@ -1780,10 +2086,122 @@ 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
-----------------------------------------------------------------------
--- [Internal] Check if a system can and should defend for HARMs itself
-- @param #MANTIS self
-- @param Wrapper.Group#GROUP targetGroup
-- @param #string targetName
-- @param Wrapper.Group#GROUP attackerGroup
-- @param #string weaponName
-- @param Wrapper.Weapon#WEAPON weaponWrapper
-- @param #number tti
-- @param #number delay
-- @return #boolean Outcome
function MANTIS:SeadAllowSuppression(targetGroup, targetName, attackerGroup, weaponName, weaponWrapper, tti, delay)
self:T(self.lid.."SeadAllowSuppression")
--- Thanks to @Goon Jan 2026
----------------------------------------------------------------
-- LOG INCOMING REQUEST
----------------------------------------------------------------
self:T(string.format("MANTIS:SeadAllowSuppression REQUEST | target=%s | weapon=%s | tti=%s | delay=%s",tostring(targetName),
tostring(weaponName),tostring(tti),tostring(delay)))
----------------------------------------------------------------
-- LOOK UP ARM CAPACITY FOR THIS SAM
----------------------------------------------------------------
local armcap = targetGroup:GetProperty("ARMCapacity")
if not armcap then
for _, sam in pairs(self.SAM_Table or {}) do
if sam[1] == targetName then
armcap = sam[7] -- ARMCapacity
break
end
end
end
self:T(string.format("MANTIS:SeadAllowSuppression SAM DATA | target=%s | ARMCapacity=%s",tostring(targetName),armcap and tostring(armcap) or "nil"))
----------------------------------------------------------------
-- TRACK SEAD THREATS (PER TARGET)
----------------------------------------------------------------
local THREAT_WINDOW = 0.1 -- seconds
self.LastThreatEval = self.LastThreatEval or {}
self.InboundARMs = self.InboundARMs or {}
local now = timer.getTime()
local last = self.LastThreatEval[targetName] or 0
if (now - last) >= THREAT_WINDOW then
self.InboundARMs[targetName] = (self.InboundARMs[targetName] or 0) + 1
self.LastThreatEval[targetName] = now
self:T(string.format("MANTIS:SeadAllowSuppression NEW threat accepted | Δt=%.3f",now - last))
else
self:T(string.format("MANTIS:SeadAllowSuppression duplicate evaluation ignored | Δt=%.3f",now - last))
end
local inbound = self.InboundARMs[targetName] or 0
self:T(string.format("MANTIS:SeadAllowSuppression THREAT COUNT | target=%s | inboundThreats=%d",tostring(targetName),inbound))
----------------------------------------------------------------
-- DECISION GATE
----------------------------------------------------------------
-- No missiles left over → legacy behavior
if targetGroup and targetGroup:IsAlive() then
local AmmotT, AmmoS, _, _,AmmoM = targetGroup:GetAmmunition()
-- TODO Check C-RAM probably needs an exception as it is a gun, need to check its effectiveness
if AmmoM and AmmoM == 0 then
self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (no MISSILES) | target=%s",tostring(targetName)))
return true
end
end
-- No ARM capacity defined → legacy behavior
if (not armcap) or armcap == 0 then
self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (no ARMCAP) | target=%s",tostring(targetName)))
return true
end
-- Suppress only once enough threats accumulated
if inbound >= armcap then
self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> APPROVED (inbound %d >= cap %d) | target=%s",inbound,armcap,tostring(targetName)))
return true
end
self:T(string.format("MANTIS:SeadAllowSuppression DECISION -> DENIED (inbound %d < cap %d) | target=%s",inbound,armcap,tostring(targetName)))
return false
end
--- [Internal] Check detection function
-- @param #MANTIS self
@@ -1807,7 +2225,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
@@ -1818,6 +2236,7 @@ do
if self.Shorad and self.Shorad.ActiveGroups and self.Shorad.ActiveGroups[name] then
activeshorad = true
end
if samgroup:GetProperty("SHORAD_ACTIVE") == true and activeshorad == false then activeshorad = true end
if IsInZone and (not suppressed) and (not activeshorad) then --check any target in zone and not currently managed by SEAD
if samgroup:IsAlive() then
-- switch on SAM
@@ -1833,19 +2252,13 @@ do
switch = true
end
if self.SamStateTracker[name] ~= "RED" and switch then
self:__RedState(1,samgroup)
self.SamStateTracker[name] = "RED"
self:__RedState(1,samgroup)
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
@@ -1872,8 +2285,8 @@ do
samgroup:OptionAlarmStateGreen()
end
if self.SamStateTracker[name] ~= "GREEN" then
self:__GreenState(1,samgroup)
self.SamStateTracker[name] = "GREEN"
self:__GreenState(1,samgroup)
end
if self.debug or self.verbose then
local text = string.format("SAM %s in alarm state GREEN!", name)
@@ -1883,12 +2296,13 @@ do
end --end alive
end --end check
end --for loop
--[[
if self.debug or self.verbose or self.logsamstatus then
for _,_status in pairs(self.SamStateTracker) do
if _status == "GREEN" then
instatusgreen=instatusgreen+1
elseif _status == "RED" then
instatusred=instatusred+1
instatusred=instatusred+1
end
end
if self.Shorad then
@@ -1897,6 +2311,8 @@ do
end
end
end
self:T(self.lid..string.format("SAM State Count: GREEN %d | RED %d | SHORAD %d",instatusred, instatusgreen, activeshorads))
--]]
return instatusred, instatusgreen, activeshorads
end
@@ -1928,13 +2344,29 @@ do
local samset = self.SAM_Table_Short -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
local instatusreds, instatusgreens, activeshoradss = self:_CheckLoop(samset,detset,dlink,self.maxshortrange)
local samset = self.SAM_Table_PointDef -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxpointdefrange)
local instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxpointdefrange)
else
local samset = self:_GetSAMTable() -- table of i.1=names, i.2=coordinates, i.3=firing range, i.4=firing height
instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxclassic)
local instatusred, instatusgreen, activeshorads = self:_CheckLoop(samset,detset,dlink,self.maxclassic)
end
local function GetReport()
if self.debug or self.verbose or self.logsamstatus then
for _,_status in pairs(self.SamStateTracker) do
if _status == "GREEN" then
instatusgreen=instatusgreen+1
elseif _status == "RED" then
instatusred=instatusred+1
end
end
if self.Shorad then
for _,_name in pairs(self.Shorad.ActiveGroups or {}) do
activeshorads=activeshorads+1
end
end
end
local statusreport = REPORT:New("\nMANTIS Status "..self.name)
statusreport:Add("+-----------------------------+")
statusreport:Add(string.format("+ SAM in RED State: %2d",instatusred))
@@ -2030,21 +2462,22 @@ 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
self.Shorad.debug = self.debug
self.Shorad:AddCallBack(self)
end
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
@@ -2242,6 +2675,7 @@ do
function MANTIS:onafterSeadSuppressionEnd(From, Event, To, Group, Name)
self:T({From, Event, To, Name})
self.SuppressedGroups[Name] = false
self.InboundARMs[Name] = 0
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
+165 -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,23 @@ 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
-- ReportTable[PlayerName] = {["Score"]=PlayerScore,["Penalty"]=PlayerPenalty}
local Playername = _playername or "Ghost"
local Score = _data.Score or 0
local Penalty = _data.Penalty or 0
text = text..string.format("%s;;%d;;%d\n",Playername,Score,Penalty)
end
UTILS.SaveToFile(path,filename,text)
end
end
--- Close CSV file
+16 -5
View File
@@ -19,7 +19,7 @@
--
-- ### Authors: **applevangelist**, **FlightControl**
--
-- Last Update: Dec 2024
-- Last Update: January 2026
--
-- ===
--
@@ -157,7 +157,7 @@ function SEAD:New( SEADGroupPrefixes, Padding )
self:AddTransition("*", "ManageEvasion", "*")
self:AddTransition("*", "CalculateHitZone", "*")
self:I("*** SEAD - Started Version 0.4.9")
self:I("*** SEAD - Started Version 0.4.11")
return self
end
@@ -318,7 +318,7 @@ function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADG
elseif height <= 12500 then
Ropt = Ropt * 0.98
end
local WeaponWrapper = WEAPON:New(SEADWeapon)
-- look at a couple of zones across the trajectory
for n=1,3 do
local dist = Ropt - ((n-1)*20000)
@@ -342,7 +342,7 @@ function SEAD:onafterCalculateHitZone(From,Event,To,SEADWeapon,pos0,height,SEADG
_targetgroupname = tgtgrp:GetName() -- group name
_targetskill = tgtgrp:GetUnit(1):GetSkill()
self:T("*** Found Target = ".. _targetgroupname)
self:ManageEvasion(_targetskill,_targetgroup,pos0,"AGM_88",SEADGroup, 20)
self:ManageEvasion(_targetskill,_targetgroup,pos0,"AGM_88",SEADGroup, 20, WeaponWrapper)
end
--end
end
@@ -442,7 +442,18 @@ function SEAD:onafterManageEvasion(From,Event,To,_targetskill,_targetgroup,SEADP
local SuppressionStartTime = timer.getTime() + delay
local SuppressionEndTime = timer.getTime() + delay + _tti + self.Padding + delay
local _targetgroupname = _targetgroup:GetName()
if not self.SuppressedGroups[_targetgroupname] then
local shoradactive = _targetgroup:GetProperty("SHORAD_ACTIVE")
if not self.SuppressedGroups[_targetgroupname] and shoradactive ~= true then
-- TODO: ask callback if suppression is allowed BEFORE scheduling timers
local allow = true
if self.UseCallBack and self.CallBack and self.CallBack.SeadAllowSuppression then
allow = self.CallBack:SeadAllowSuppression(_targetgroup,_targetgroupname,SEADGroup,SEADWeaponName,Weapon,_tti,delay)
end
if not allow then
self:T(string.format("*** SEAD - %s | Suppression vetoed by callback", _targetgroupname))
-- Important: do NOT schedule SuppressionStart/Stop and do NOT flip SuppressedGroups true.
return self
end
self:T(string.format("*** SEAD - %s | Parameters TTI %ds | Switch-Off in %ds",_targetgroupname,_tti,delay))
timer.scheduleFunction(SuppressionStart,{_targetgroup,_targetgroupname, SEADGroup},SuppressionStartTime)
timer.scheduleFunction(SuppressionStop,{_targetgroup,_targetgroupname},SuppressionEndTime)
+132 -59
View File
@@ -21,7 +21,7 @@
-- @image Functional.Shorad.jpg
--
-- Date: Nov 2021
-- Last Update: Jan 2025
-- Last Update: Jan 2026
-------------------------------------------------------------------------
--- **SHORAD** class, extends Core.Base#BASE
@@ -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.6")
-- Set the string id for output to DCS.log file.
self.lid=string.format("SHORAD %s | ", self.name)
self:_InitState()
@@ -451,6 +465,43 @@ do
return returnname
end
--- Set an object to call back when going evasive.
-- @param #SHORAD self
-- @param #table Object The object to call.
-- @return #SHORAD self
function SHORAD:AddCallBack(Object)
self:T({Class=Object.ClassName})
self.CallBack = Object
self.UseCallBack = true
return self
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
@@ -484,7 +535,56 @@ do
-- mymantis:Start()
function SHORAD:onafterWakeUpShorad(From, Event, To, TargetGroup, Radius, ActiveTimer, TargetCat, ShotAt)
self:T(self.lid .. " WakeUpShorad")
self:T({TargetGroup, Radius, ActiveTimer, TargetCat})
--self:T({TargetGroup, Radius, ActiveTimer, TargetCat})
local TDiff = 4
-- local function to switch off shorad again
local function SleepShorad(group)
if group and group:IsAlive() then
local groupname = group:GetName()
self.ActiveGroups[groupname] = nil
if self.UseEmOnOff then
group:EnableEmission(false)
else
group:OptionAlarmStateGreen()
end
group:SetProperty("SHORAD_ACTIVE",false)
local text = string.format("Sleeping SHORAD %s", group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
--Shoot and Scoot
if self.shootandscoot then
self:__ShootAndScoot(1,group)
else
--group:RelocateGroundRandomInRadius(30,500,false,true,"Diamond",true)
end
end
end
local function WakeUp(_group,groupname)
-- shot at a group we protect
local text = string.format("Waking up SHORAD %s", _group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
if self.UseEmOnOff then
_group:EnableEmission(true)
end
_group:OptionAlarmStateRed()
_group:SetProperty("SHORAD_ACTIVE",true)
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
self.ActiveGroups[groupname].Timer = TIMER:New(SleepShorad,_group):Start(endtime)
--Shoot and Scoot
if self.shootandscoot then
self:__ShootAndScoot(TDiff,_group)
TDiff=TDiff+1
end
end
end
local targetcat = TargetCat or Object.Category.UNIT
local targetgroup = TargetGroup
local targetvec2 = nil
@@ -501,69 +601,41 @@ do
local groupset = self.Groupset --Core.Set#SET_GROUP
local shoradset = groupset:GetAliveSet() --#table
-- local function to switch off shorad again
local function SleepShorad(group)
if group and group:IsAlive() then
local groupname = group:GetName()
self.ActiveGroups[groupname] = nil
if self.UseEmOnOff then
group:EnableEmission(false)
else
group:OptionAlarmStateGreen()
end
local text = string.format("Sleeping SHORAD %s", group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
--Shoot and Scoot
if self.shootandscoot then
self:__ShootAndScoot(1,group)
end
end
end
-- go through set and find the one(s) to activate
local TDiff = 4
for _,_group in pairs (shoradset) do
local groupname = _group:GetName()
if groupname == TargetGroup and ShotAt==true then
-- Shot at a SHORAD group
if self.UseEmOnOff then
_group:EnableEmission(false)
local allow = false
if self.CallBack and self.UseCallBack == true then
allow = self.CallBack:SeadAllowSuppression(_group,groupname)
end
_group:OptionAlarmStateGreen()
self.ActiveGroups[groupname] = nil
local text = string.format("Shot at SHORAD %s! Evading!", _group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
--Shoot and Scoot
if self.shootandscoot then
self:__ShootAndScoot(1,_group)
end
elseif _group:IsAnyInZone(targetzone) or groupname == TargetGroup then
-- shot at a group we protect
local text = string.format("Waking up SHORAD %s", _group:GetName())
self:T(text)
local m = MESSAGE:New(text,10,"SHORAD"):ToAllIf(self.debug)
if self.UseEmOnOff then
_group:EnableEmission(true)
end
_group:OptionAlarmStateRed()
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
self.ActiveGroups[groupname].Timer = TIMER:New(SleepShorad,_group):Start(endtime)
if allow == true then
if self.UseEmOnOff then
_group:EnableEmission(false)
end
_group:OptionAlarmStateGreen()
self.ActiveGroups[groupname] = nil
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(TDiff,_group)
TDiff=TDiff+1
self:__ShootAndScoot(1,_group)
else
_group:RelocateGroundRandomInRadius(30,500,false,true,"Diamond",true)
end
else
WakeUp(_group,groupname)
end
end
end
elseif _group:IsAnyInZone(targetzone) or groupname == TargetGroup then
WakeUp(_group,groupname)
end -- end if
end -- end in pairs
return self
end
@@ -679,11 +751,12 @@ do
-- @param Core.Event#EVENTDATA EventData The event details table data set
-- @return #SHORAD self
function SHORAD:HandleEventShot( EventData )
self:T( { EventData } )
--self:T( { EventData.id } )
self:T(self.lid .. " HandleEventShot")
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
@@ -703,7 +776,7 @@ do
-- Is there target data?
if not targetdata or self.debug then
if string.find(ShootingWeaponName,"AGM_88",1,true) then
self:I("**** Tracking AGM-88 with no target data.")
self:T("**** Tracking AGM-88 with no target data.")
local pos0 = EventData.IniUnit:GetCoordinate()
local fheight = EventData.IniUnit:GetHeight()
self:__CalculateHitZone(20,ShootingWeapon,pos0,fheight,EventData.IniGroup)
@@ -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
@@ -8596,8 +8597,8 @@ function WAREHOUSE:_DeleteStockItem(stockitem)
local item=self.stock[i] --#WAREHOUSE.Assetitem
if item.uid==stockitem.uid then
table.remove(self.stock,i)
-- remove also from warehouse DB
_WAREHOUSEDB.Assets[stockitem.uid]=nil
-- remove also from warehouse DB (not good! causes an error if the asset needs to be added to a requesting wherehouse)
--_WAREHOUSEDB.Assets[stockitem.uid]=nil
break
end
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' )
@@ -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
+36
View File
@@ -326,6 +326,42 @@ 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 = Squadron.templategroup
if group then
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.warehouse and storage:IsLimitedAircraft() and typename ~= "none" then
local NInStore = storage:GetItemAmount(typename) or 0
if NAssets > NInStore then
storage:AddItem(typename,NAssets)
end
end
local unit = group:GetUnit(1)
-- if storage is limited, add the amount of liquids needed
if unit and storage and storage.warehouse and storage:IsLimitedLiquids() and typename ~= "none" then
local fuel = unit:GetFuelMassMax()
local neededfuel = (fuel*NAssets) -- kgs of fuel
local NInStore = storage:GetLiquidAmount(STORAGE.Liquid.JETFUEL) or 0
self:T(string.format(self.lid.."Fuel Needed: %dt | Fuel in store: %dt",neededfuel/1000,NInStore/1000))
if neededfuel > NInStore then
storage:AddLiquid(STORAGE.Liquid.JETFUEL,neededfuel)
end
end
end
end
return self
end
+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)
+85 -32
View File
@@ -31,7 +31,7 @@
-- @image OPS_CSAR.jpg
---
-- Last Update Oct 2025
-- Last Update Jan 2026
-------------------------------------------------------------------------
--- **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.
@@ -312,10 +315,12 @@ CSAR.AircraftType["MH-60R"] = 10
CSAR.AircraftType["OH-6A"] = 2
CSAR.AircraftType["OH58D"] = 2
CSAR.AircraftType["CH-47Fbl1"] = 31
CSAR.AircraftType["AH-6J"] = 2
CSAR.AircraftType["MH-6J"] = 2
--- CSAR class version.
-- @field #string version
CSAR.version="1.0.34"
CSAR.version="1.0.36"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
@@ -431,12 +436,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 +679,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 +857,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 +1242,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 +1277,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 +1771,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 +2155,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 +2209,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 +2238,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 +2313,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 +2336,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 +2436,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 +2564,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 +2906,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 +2917,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 +3032,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
+11 -1
View File
@@ -396,7 +396,8 @@ function CHIEF:New(Coalition, AgentSet, Alias)
self.TransportCategories = {Group.Category.HELICOPTER}
-- Create a new COMMANDER.
self.commander=COMMANDER:New(Coalition)
self.commander=COMMANDER:New(Coalition, Alias)
-- Add FSM transitions.
-- From State --> Event --> To State
@@ -3357,6 +3358,15 @@ function CHIEF._CheckAssetProperties(Asset, Properties)
return false
end
--- Checks whether or not any of the legions con run a mission.
-- @param #CHIEF self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If `true`, one of the cohorts can run the mission.
function CHIEF:CanMission(Mission)
return self.commander and self.commander:CanMission(Mission)
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
+64
View File
@@ -1748,6 +1748,70 @@ function COMMANDER:_GetCohorts(Legions, Cohorts, Operation)
return cohorts
end
--- Checks whether or not any of the legions con run a mission.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
-- @return #boolean If `true`, one of the cohorts can run the mission.
function COMMANDER:CanMission(Mission)
local commander = self
-- Target position.
local TargetVec2 = Mission:GetTargetVec2()
local MaxWeight = nil
if Mission.NcarriersMin then
local legions = commander.legions
local cohorts = nil
if Mission.transportLegions or Mission.transportCohorts then
legions = Mission.transportLegions
cohorts = Mission.transportCohorts
end
-- Get transport cohorts.
local Cohorts = LEGION._GetCohorts(legions, cohorts)
-- Filter cohorts that can actually perform transport missions.
local transportcohorts = {}
for _, _cohort in pairs(Cohorts) do
local cohort = _cohort --Ops.Cohort#COHORT
-- Check if cohort can perform transport to target.
local can = LEGION._CohortCan(cohort, AUFTRAG.Type.OPSTRANSPORT, Mission.carrierCategories, Mission.carrierAttributes, Mission.carrierProperties, nil, TargetVec2)
-- MaxWeight of cargo assets is limited by the largets available cargo bay. We don't want to select, e.g., tanks that cannot be transported by APCs or helos.
if can and (MaxWeight == nil or cohort.cargobayLimit > MaxWeight) then
MaxWeight = cohort.cargobayLimit
end
end
end
local legions = commander.legions
local cohorts = nil
if Mission.specialLegions or Mission.specialCohorts then
legions = Mission.specialLegions
cohorts = Mission.specialCohorts
end
-- Get cohorts.
local Cohorts = LEGION._GetCohorts(legions, cohorts, Mission.operation, commander.opsqueue)
for _, _cohort in pairs(Cohorts) do
local cohort = _cohort --Ops.Cohort#COHORT
-- Check if cohort can do the mission.
local can = LEGION._CohortCan(cohort, Mission.type, nil, Mission.attributes, Mission.properties, { Mission.engageWeaponType }, TargetVec2, Mission.engageRange, Mission.refuelSystem, nil, MaxWeight)
if can then
return true
end
end
return false
end
--- Recruit assets for a given mission.
-- @param #COMMANDER self
-- @param Ops.Auftrag#AUFTRAG Mission The mission.
+950
View File
@@ -0,0 +1,950 @@
--- **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: Jan 2026
-------------------------------------------------------------------------
--
-- ===
--
-- **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.4"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- 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"
-- Set some string id for output to DCS.log file.
self.lid=string.format("EASYA2G %s | ", self.alias)
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"}
self:SetDefaultTurnoverTime()
-- 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(self.maintenancetime,self.repairtime)
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
+165 -18
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
-------------------------------------------------------------------------
@@ -12,7 +21,7 @@
--
-------------------------------------------------------------------------
-- Date: September 2023
-- Last Update: Aug 2025
-- Last Update: Jan 2026
-------------------------------------------------------------------------
--
--- **Ops** - Easy GCI & CAP Manager
@@ -80,6 +89,8 @@
-- @field #number FuelCriticalThreshold
-- @field #boolean showpatrolpointmarks
-- @field #table EngageTargetTypes
-- @field #number maintenancetime
-- @field #number repairtime
-- @extends Core.Fsm#FSM
--- *“Airspeed, altitude, and brains. Two are always needed to successfully complete the flight.”* -- Unknown.
@@ -109,7 +120,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 +216,7 @@ EASYGCICAP = {
coalition = "blue",
alias = nil,
wings = {},
Intel = nil,
Intel = nil, -- Ops.Intel#INTEL
resurrection = 900,
capspeed = 300,
capalt = 25000,
@@ -275,10 +286,11 @@ EASYGCICAP = {
--- EASYGCICAP class version.
-- @field #string version
EASYGCICAP.version="0.1.30"
EASYGCICAP.version="0.1.34"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
--
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: TBD
@@ -300,6 +312,10 @@ function EASYGCICAP:New(Alias, AirbaseName, Coalition, EWRName)
-- defaults
self.alias = Alias or AirbaseName.." CAP Wing"
-- Set some string id for output to DCS.log file.
self.lid=string.format("EASYGCICAP %s | ", self.alias)
self.coalitionname = string.lower(Coalition) or "blue"
self.coalition = self.coalitionname == "blue" and coalition.side.BLUE or coalition.side.RED
self.wings = {}
@@ -333,10 +349,8 @@ function EASYGCICAP:New(Alias, AirbaseName, Coalition, EWRName)
self.FuelCriticalThreshold = 10
self.showpatrolpointmarks = false
self.EngageTargetTypes = {"Air"}
self:SetDefaultTurnoverTime()
-- Set some string id for output to DCS.log file.
self.lid=string.format("EASYGCICAP %s | ", self.alias)
-- Add FSM transitions.
-- From State --> Event --> To State
self:SetStartState("Stopped")
@@ -350,6 +364,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 +411,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
@@ -548,6 +612,18 @@ function EASYGCICAP:SetDefaultMissionRange(Range)
return self
end
--- Set default turnover times for squadrons in minutes
-- @param #EASYGCICAP self
-- @param #number MaintenanceTime Time in minutes it takes until a flight is combat ready again. Default is 5 min.
-- @param #number RepairTime Time in minutes it takes to repair a flight for each life point taken. Default is 10 min.
-- @return #EASYGCICAP self
function EASYGCICAP:SetDefaultTurnoverTime(MaintenanceTime,RepairTime)
self:T(self.lid.."SetDefaultTurnoverTime")
self.maintenancetime=MaintenanceTime or 5
self.repairtime=RepairTime or 10
return self
end
--- Set default number of airframes standing by for intercept tasks (visible on the airfield)
-- @param #EASYGCICAP self
-- @param #number Airframes defaults to 2
@@ -611,7 +687,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 +731,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 +838,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)
@@ -1153,7 +1233,7 @@ function EASYGCICAP:_AddSquadron(TemplateName, SquadName, AirbaseName, AirFrames
Squadron_One:AddMissionCapability({AUFTRAG.Type.CAP, AUFTRAG.Type.GCICAP, AUFTRAG.Type.INTERCEPT, AUFTRAG.Type.PATROLRACETRACK, AUFTRAG.Type.ALERT5})
--Squadron_One:SetFuelLowRefuel(true)
Squadron_One:SetFuelLowThreshold(0.3)
Squadron_One:SetTurnoverTime(10,20)
Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime)
Squadron_One:SetModex(Modex)
Squadron_One:SetLivery(Livery)
Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
@@ -1184,7 +1264,7 @@ function EASYGCICAP:_AddReconSquadron(TemplateName, SquadName, AirbaseName, AirF
Squadron_One:AddMissionCapability({AUFTRAG.Type.RECON})
--Squadron_One:SetFuelLowRefuel(true)
Squadron_One:SetFuelLowThreshold(0.3)
Squadron_One:SetTurnoverTime(10,20)
Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime)
Squadron_One:SetModex(Modex)
Squadron_One:SetLivery(Livery)
Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
@@ -1218,7 +1298,7 @@ function EASYGCICAP:_AddTankerSquadron(TemplateName, SquadName, AirbaseName, Air
Squadron_One:AddMissionCapability({AUFTRAG.Type.TANKER})
--Squadron_One:SetFuelLowRefuel(true)
Squadron_One:SetFuelLowThreshold(0.3)
Squadron_One:SetTurnoverTime(10,20)
Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime)
Squadron_One:SetModex(Modex)
Squadron_One:SetLivery(Livery)
Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
@@ -1255,7 +1335,7 @@ function EASYGCICAP:_AddAWACSSquadron(TemplateName, SquadName, AirbaseName, AirF
Squadron_One:AddMissionCapability({AUFTRAG.Type.AWACS})
--Squadron_One:SetFuelLowRefuel(true)
Squadron_One:SetFuelLowThreshold(0.3)
Squadron_One:SetTurnoverTime(10,20)
Squadron_One:SetTurnoverTime(self.maintenancetime,self.repairtime)
Squadron_One:SetModex(Modex)
Squadron_One:SetLivery(Livery)
Squadron_One:SetSkill(Skill or AI.Skill.AVERAGE)
@@ -1300,6 +1380,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 +1587,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 +1598,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 +1638,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:SetCorridorLimits(self.corridorfloor,self.corridorceiling)
end
end
BlueIntel:Start()
if self.debug then
@@ -1603,7 +1749,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 +1760,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
+172 -10
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
@@ -1040,7 +1175,7 @@ function INTEL:_CreateContact(Positionable, RecceName)
item.category=3 --static:GetCategory()
item.categoryname=static:GetCategoryName() or "Unknown"
item.threatlevel=static:GetThreatLevel() or 0
item.position=static:GetCoordinate()
item.position=static:GetCoord()
item.velocity=static:GetVelocityVec3()
item.speed=0
item.recce=RecceName
@@ -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
@@ -1147,15 +1282,15 @@ function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisua
local DetectionAccepted = true
if self.RadarAcceptRange then
local reccecoord = Unit:GetCoordinate()
local coord = unit:GetCoordinate()
local reccecoord = Unit:GetCoord()
local coord = unit:GetCoord()
local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km
if dist > self.RadarAcceptRangeKilometers then DetectionAccepted = false end
end
if self.RadarBlur then
local reccecoord = Unit:GetCoordinate()
local coord = unit:GetCoordinate()
local reccecoord = Unit:GetCoord()
local coord = unit:GetCoord()
local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km
local AGL = unit:GetAltitude(true)
local minheight = self.RadarBlurMinHeight or 250 -- meters
@@ -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.
+379 -36
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
@@ -235,7 +237,7 @@ function PLAYERTASK:New(Type, Target, Repeat, Times, TTSType)
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #boolen Silent If true, suppress message output on cancel.
-- @param #boolean Silent If true, suppress message output on cancel.
--- On After "Planned" event. Task has been planned.
-- @function [parent=#PLAYERTASK] OnAfterPilotPlanned
@@ -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
---
@@ -1583,6 +1599,7 @@ do
-- ELEVATION = "\nTarget Elevation: %s %s",
-- METER = "meter",
-- FEET = "feet",
-- INTERCEPTCOURSE = "Intercept course",
-- },
--
-- e.g.
@@ -1669,8 +1686,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 +1757,12 @@ PLAYERTASKCONTROLLER = {
MenuNoTask = nil,
InformationMenu = false,
TaskInfoDuration = 30,
TaskPersistance = {},
TaskPersistanceSwitch = false,
TaskPersistancePath = nil,
TaskPersistanceFilename = nil,
TasksPersistable = {},
SceneryExplosivesAmount = 300,
}
---
@@ -1740,6 +1783,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 +1803,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
@@ -1860,6 +1922,7 @@ PLAYERTASKCONTROLLER.Messages = {
DESTROYER = "Destroyer",
CARRIER = "Aircraft Carrier",
RADIOS = "Radios",
INTERCEPTCOURSE = "Intercept course",
},
DE = {
TASKABORT = "Auftrag abgebrochen!",
@@ -1947,12 +2010,13 @@ PLAYERTASKCONTROLLER.Messages = {
DESTROYER = "Zerstörer",
CARRIER = "Flugzeugträger",
RADIOS = "Frequenzen",
INTERCEPTCOURSE = "Abfangkurs",
},
}
--- PLAYERTASK class version.
-- @field #string version
PLAYERTASKCONTROLLER.version="0.1.71"
PLAYERTASKCONTROLLER.version="0.1.73"
--- Create and run a new TASKCONTROLLER instance.
-- @param #PLAYERTASKCONTROLLER self
@@ -2059,7 +2123,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 +2238,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 +2949,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 +3645,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
@@ -3700,6 +3779,41 @@ function PLAYERTASKCONTROLLER:_ShowRadioInfo(Group, Client)
return self
end
--- Calculate group future position after given seconds.
-- @param #PLAYERTASKCONTROLLER self
-- @param Wrapper.Group#GROUP group The group to calculate for.
-- @param #number seconds Time interval in seconds. Default is `self.prediction`.
-- @return Core.Point#COORDINATE Calculated future position of the cluster.
function PLAYERTASKCONTROLLER:_CalcGroupFuturePosition(group, seconds)
-- Get current position of the cluster.
local p=group:GetCoordinate()
-- Velocity vector in m/s.
local v=group:GetVelocityVec3()
-- Time in seconds.
local t=seconds or self.prediction
-- Extrapolated vec3.
local Vec3={x=p.x+v.x*t, y=p.y+v.y*t, z=p.z+v.z*t}
-- Future position.
local futureposition=COORDINATE:NewFromVec3(Vec3)
-- Create an arrow pointing in the direction of the movement.
if self.verbose == true then
local markerID = group:GetProperty("PLAYERTASK_ARROW")
if markerID then
COORDINATE:RemoveMark(markerID)
end
markerID = p:ArrowToAll(futureposition, self.coalition, {1,0,0}, 1, {1,1,0}, 0.5, 2, true, "Position Calc")
group:SetProperty("PLAYERTASK_ARROW",markerID)
end
return futureposition
end
--- [Internal] Flashing directional info for a client
-- @param #PLAYERTASKCONTROLLER self
-- @return #PLAYERTASKCONTROLLER self
@@ -3709,16 +3823,34 @@ function PLAYERTASKCONTROLLER:_FlashInfo()
if _client and _client:IsAlive() then
if self.TasksPerPlayer:HasUniqueID(_playername) then
local task = self.TasksPerPlayer:ReadByID(_playername) -- Ops.PlayerTask#PLAYERTASK
local Coordinate = task.Target:GetCoordinate()
local Coordinate = task.Target:GetCoordinate() -- Core.Point#COORDINATE
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)
local targettxt = self.gettext:GetEntry("TARGET",self.locale)
local text = targettxt..": "..CoordText
local m = MESSAGE:New(text,10,"Tasking"):ToClient(_client)
else
CoordText = Coordinate:ToStringA2A(_client, nil, self.ShowMagnetic)
local targettxt = self.gettext:GetEntry("TARGET",self.locale)
local text = targettxt..": "..CoordText
-- calc intercept position
local name=task.Target:GetName()
local group = GROUP:FindByName(name)
local clientcoord = _client:GetCoordinate()
if group and clientcoord and group:IsAlive() and task.Type==AUFTRAG.Type.INTERCEPT then
local speed = math.max(UTILS.KnotsToMps(350) or _client:GetVelocityMPS())
local dist = Coordinate:Get3DDistance(clientcoord)
local iTime = math.floor(dist/speed)+5
if iTime < 10 then iTime = 10
elseif iTime > 600 then iTime = 600 end
local npos = self:_CalcGroupFuturePosition(group,iTime)
local BR = npos:ToStringBearing(clientcoord,nil,self.ShowMagnetic,0 )
local Intercepttext = self.gettext:GetEntry("INTERCEPTCOURSE",self.locale)
text = text .. "\n"..Intercepttext.." "..BR
end
local m = MESSAGE:New(text,10,"Tasking"):ToClient(_client)
end
local targettxt = self.gettext:GetEntry("TARGET",self.locale)
local text = "Target: "..CoordText
local m = MESSAGE:New(text,10,"Tasking"):ToClient(_client)
end
end
end
@@ -3770,7 +3902,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)
@@ -3807,8 +3939,10 @@ function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client)
Elevation = math.floor(UTILS.MetersToFeet(Elevation))
end
-- ELEVATION = "\nTarget Elevation: %s %s",
local elev = self.gettext:GetEntry("ELEVATION",self.locale)
text = text .. string.format(elev,tostring(math.floor(Elevation)),elevationmeasure)
if task.Type ~= AUFTRAG.Type.INTERCEPT then
local elev = self.gettext:GetEntry("ELEVATION",self.locale)
text = text .. string.format(elev,tostring(math.floor(Elevation)),elevationmeasure)
end
-- Prec bombing
if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then
if LasingDrone and LasingDrone.playertask then
@@ -4584,6 +4718,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 +4969,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 +5114,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 +5159,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 +5319,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
+34 -4
View File
@@ -310,7 +310,7 @@ _RECOVERYTANKERID=0
--- Class version.
-- @field #string version
RECOVERYTANKER.version="1.0.10"
RECOVERYTANKER.version="1.0.11"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@@ -923,6 +923,22 @@ function RECOVERYTANKER:onafterStart(From, Event, To)
Spawn:InitRadioCommsOnOff(true)
Spawn:InitRadioFrequency(self.RadioFreq)
Spawn:InitRadioModulation(self.RadioModu)
if self.callsignname and self.callsignnumber then
local grp = GROUP:FindByName(self.tankergroupname)
if grp then
local typename = grp:GetTypeName() or ""
--self:I(self.lid.."Typename: "..typename)
local Name
local enumerator = CALLSIGN.Tanker
if typename == "A6E" then
enumerator = CALLSIGN.Intruder
end
Name = self:_GetCallsignName(self.callsignname,enumerator)
--self:I(self.lid.."CallsignName: "..Name)
Spawn:InitCallSign(self.callsignname,Name,self.callsignnumber,self.callsignnumber)
end
end
Spawn:InitModex(self.modex)
-- Spawn on carrier.
@@ -1190,9 +1206,9 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To)
-- Task combo.
-- Be a tanker or be an AWACS.
local taskroll = self.tanker:EnRouteTaskTanker()
local taskrole = self.tanker:EnRouteTaskTanker()
if self.awacs then
taskroll=self.tanker:EnRouteTaskAWACS()
taskrole=self.tanker:EnRouteTaskAWACS()
end
--local taskeplrs=self.tanker:TaskEPLRS(true, 2)
@@ -1201,7 +1217,7 @@ function RECOVERYTANKER:onafterPatternUpdate(From, Event, To)
local taskroute = self.tanker:TaskRoute(wp)
-- Note that the order is important here! tasktanker has to come first. Otherwise it does not work.
local taskcombo = self.tanker:TaskCombo({taskroll, taskroute})
local taskcombo = self.tanker:TaskCombo({taskrole, taskroute})
-- Set task.
self.tanker:SetTask(taskcombo, 1)
@@ -1450,6 +1466,20 @@ function RECOVERYTANKER:_RefuelingStop(EventData)
end
--- Get clear name callsign for spawn from enumerator
-- @param #RECOVERYTANKER self
-- @param #number Callsign
-- @param #table Enumerator The table of callsigns, e.g. `CALLSIGN.Tanker`
-- @return #string Name Name or "" if not found
function RECOVERYTANKER:_GetCallsignName(Callsign, Enumerator)
for name, value in pairs(Enumerator or {}) do
if value==Callsign then
return name
end
end
return ""
end
--- A unit crashed or died.
-- @param #RECOVERYTANKER self
-- @param Core.Event#EVENTDATA EventData Event data.
+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()
+303 -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",
@@ -528,6 +529,7 @@ ENUMS.ReportingName =
Orion = "P-3C",
Viking = "S-3B",
Osprey = "V-22",
Intruder = "A6E",
-- Bomber Rus
Badger = "H6-J",
Bear_J = "Tu-142", -- also Tu-95
@@ -1772,6 +1774,297 @@ 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'
ENUMS.Storage.weapons.adapters.HB_F_4E_ORD_LAU_77 = 'weapons.adapters.HB_F-4E_ORD_LAU_77'
ENUMS.Storage.weapons.adapters.hb_a_6e_lau7_adu299 = 'weapons.adapters.hb_a-6e_lau7_adu299'
ENUMS.Storage.weapons.bombs.AH6_SMOKE_BLUE = 'weapons.bombs.AH6_SMOKE_BLUE'
ENUMS.Storage.weapons.bombs.AH6_SMOKE_GREEN = 'weapons.bombs.AH6_SMOKE_GREEN'
ENUMS.Storage.weapons.bombs.AH6_SMOKE_RED = 'weapons.bombs.AH6_SMOKE_RED'
ENUMS.Storage.weapons.bombs.AH6_SMOKE_YELLOW = 'weapons.bombs.AH6_SMOKE_YELLOW'
ENUMS.Storage.weapons.missiles.HB_AGM_78 = 'weapons.missiles.HB_AGM_78'
ENUMS.Storage.weapons.missiles.V_1 = 'weapons.missiles.V-1'
ENUMS.Storage.weapons.shells.Oerlikon_20mm_HE = 'weapons.shells.Oerlikon_20mm_HE'
ENUMS.Storage.weapons.shells.RM_15cm_HE = 'weapons.shells.RM_15cm_HE'
ENUMS.Storage.weapons.shells.HE_T_MkII_40mm = 'weapons.shells.HE_T_MkII_40mm'
ENUMS.Storage.weapons.shells.APCBC = 'weapons.shells.APCBC'
ENUMS.Storage.weapons.shells.Sprgr_43_L71 = 'weapons.shells.Sprgr_43_L71'
ENUMS.Storage.weapons.shells.Mk_20_HE_shell = 'weapons.shells.Mk_20_HE_shell'
ENUMS.Storage.weapons.shells.Pzgr_39_42 = 'weapons.shells.Pzgr_39/42'
ENUMS.Storage.weapons.shells.M1_37mm_37AP_T = 'weapons.shells.M1_37mm_37AP-T'
ENUMS.Storage.weapons.shells.Sprgr_38 = 'weapons.shells.Sprgr_38'
ENUMS.Storage.weapons.shells.Sprgr_39 = 'weapons.shells.Sprgr_39'
ENUMS.Storage.weapons.shells.Pzgr_39_43 = 'weapons.shells.Pzgr_39/43'
ENUMS.Storage.weapons.shells.Pzgr_39_5cm = 'weapons.shells.Pzgr_39_5cm'
ENUMS.Storage.weapons.shells.AP_2A20_115mm = 'weapons.shells.2A20_115mm_AP'
ENUMS.Storage.weapons.shells.AP_T_MkI_40mm = 'weapons.shells.AP_T_MkI_40mm'
ENUMS.Storage.weapons.shells.AP_37x263 = 'weapons.shells.37x263_AP'
ENUMS.Storage.weapons.shells.AP_20x138B = 'weapons.shells.20x138B_AP'
ENUMS.Storage.weapons.shells.Besa7_92x57T = 'weapons.shells.Besa7_92x57T'
ENUMS.Storage.weapons.shells.Sprgr_34_L70 = 'weapons.shells.Sprgr_34_L70'
ENUMS.Storage.weapons.shells.QF94_AA_HE = 'weapons.shells.QF94_AA_HE'
ENUMS.Storage.weapons.shells.AH_6762x51mm_M62 = 'weapons.shells.AH-6 7.62x51mm M62'
ENUMS.Storage.weapons.shells.AH_6762x51mm_M80 = 'weapons.shells.AH-6 7.62x51mm M80'
ENUMS.Storage.weapons.shells.AH_6_762x51mm_M61 = 'weapons.shells.AH-6 7.62x51mm M61'
ENUMS.Storage.weapons.shells.leFH18_105HE = 'weapons.shells.leFH18_105HE'
ENUMS.Storage.weapons.shells.M63_37HE = 'weapons.shells.M63_37HE'
ENUMS.Storage.weapons.shells.QF95_206R_fixed = 'weapons.shells.QF95_206R_fixed'
ENUMS.Storage.weapons.shells.UBR_365_85AP = 'weapons.shells.UBR_365_85AP'
ENUMS.Storage.weapons.shells.M101 = 'weapons.shells.M101'
ENUMS.Storage.weapons.shells.HE_M1_Shell = 'weapons.shells.HE_M1_Shell'
ENUMS.Storage.weapons.shells.UO_365K_85HE = 'weapons.shells.UO_365K_85HE'
ENUMS.Storage.weapons.shells.Flak41_Sprgr_39 = 'weapons.shells.Flak41_Sprgr_39'
ENUMS.Storage.weapons.shells.M1_37mm_HE_T = 'weapons.shells.M1_37mm_HE-T'
ENUMS.Storage.weapons.shells.QF17_HE = 'weapons.shells.QF17_HE'
ENUMS.Storage.weapons.shells.Pzgr_39 = 'weapons.shells.Pzgr_39'
ENUMS.Storage.weapons.shells.Besa7_92x57 = 'weapons.shells.Besa7_92x57'
ENUMS.Storage.weapons.shells.I_Gr_33 = 'weapons.shells.I_Gr_33'
ENUMS.Storage.weapons.shells.M62_APC = 'weapons.shells.M62_APC'
ENUMS.Storage.weapons.shells.Mk_12_HE_shell = 'weapons.shells.Mk_12_HE_shell'
ENUMS.Storage.weapons.shells.M51_37AP = 'weapons.shells.M51_37AP'
ENUMS.Storage.weapons.shells.M42A1_HE = 'weapons.shells.M42A1_HE'
ENUMS.Storage.weapons.shells.HE_20x138B = 'weapons.shells.20x138B_HE'
ENUMS.Storage.weapons.shells.HE_37x263 = 'weapons.shells.37x263_HE'
ENUMS.Storage.weapons.shells.HE_2A20_115mm = 'weapons.shells.2A20_115mm_HE'
ENUMS.Storage.weapons.gunmounts.B17_TailTurret_M2_L = 'weapons.gunmounts.B17_TailTurret_M2_L'
ENUMS.Storage.weapons.gunmounts.AH6_M134L = 'weapons.gunmounts.{AH6_M134L}'
ENUMS.Storage.weapons.gunmounts.B17_Left_Nose_M2 = 'weapons.gunmounts.B17_Left_Nose_M2'
ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Top_Right_MG_81 = 'weapons.gunmounts.Ju88_Turret_Top_Right_MG_81'
ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Bottom_MG_81_L = 'weapons.gunmounts.Ju88_Turret_Bottom_MG_81_L'
ENUMS.Storage.weapons.gunmounts.B17_ChinTurret_M2_R = 'weapons.gunmounts.B17_ChinTurret_M2_R'
ENUMS.Storage.weapons.gunmounts.B17_Waist_Right_M2 = 'weapons.gunmounts.B17_Waist_Right_M2'
ENUMS.Storage.weapons.gunmounts.AH_6_Door_Gun = 'weapons.gunmounts.AH-6_Door_Gun'
ENUMS.Storage.weapons.gunmounts.B17_BallTurret_M2_L = 'weapons.gunmounts.B17_BallTurret_M2_L'
ENUMS.Storage.weapons.gunmounts.B17_BallTurret_M2_R = 'weapons.gunmounts.B17_BallTurret_M2_R'
ENUMS.Storage.weapons.gunmounts.B17_TopTurret_M2_R = 'weapons.gunmounts.B17_TopTurret_M2_R'
ENUMS.Storage.weapons.gunmounts.B17_Right_Nose_M2 = 'weapons.gunmounts.B17_Right_Nose_M2'
ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Bottom_MG_81_R = 'weapons.gunmounts.Ju88_Turret_Bottom_MG_81_R'
ENUMS.Storage.weapons.gunmounts.B17_TopTurret_M2_L = 'weapons.gunmounts.B17_TopTurret_M2_L'
ENUMS.Storage.weapons.gunmounts.B17_Waist_Left_M2 = 'weapons.gunmounts.B17_Waist_Left_M2'
ENUMS.Storage.weapons.gunmounts.Ju88_Turret_ahead_MG_81 = 'weapons.gunmounts.Ju88_Turret_ahead_MG_81'
ENUMS.Storage.weapons.gunmounts.AH6_M134R = 'weapons.gunmounts.{AH6_M134R}'
ENUMS.Storage.weapons.gunmounts.Ju88_Turret_Top_Left_MG_81 = 'weapons.gunmounts.Ju88_Turret_Top_Left_MG_81'
ENUMS.Storage.weapons.gunmounts.B17_TailTurret_M2_R = 'weapons.gunmounts.B17_TailTurret_M2_R'
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.AKAN_NO_TRC'
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.AKAN'
ENUMS.Storage.weapons.gunmounts.B17_ChinTurret_M2_L = 'weapons.gunmounts.B17_ChinTurret_M2_L'
ENUMS.Storage.weapons.gunmounts.AKAN = 'weapons.gunmounts.AKAN'
ENUMS.Storage.weapons.gunmounts.AKAN_NO_TRC = 'weapons.gunmounts.AKAN_NO_TRC'
ENUMS.Storage.weapons.gunmounts.AH_6_Door = 'weapons.gunmounts.{AH-6_Door}'
ENUMS.Storage.weapons.gunmounts.AH_6_FN_HMP400 = 'weapons.gunmounts.{AH-6_FN_HMP400}'
ENUMS.Storage.weapons.gunmounts.AH_6_M134L = 'weapons.gunmounts.AH-6_M134L'
ENUMS.Storage.weapons.gunmounts.AH_6_M134R = 'weapons.gunmounts.AH-6_M134R'
ENUMS.Storage.weapons.gunmounts.AH_6_HMP400 = 'weapons.gunmounts.AH-6_HMP400'
ENUMS.Storage.weapons.droptanks.PTB_800 = 'weapons.droptanks.PTB-800'
ENUMS.Storage.weapons.droptanks.PTB_275 = 'weapons.droptanks.PTB-275'
ENUMS.Storage.weapons.droptanks.HB_A6E_AERO1D_EMPTY = 'weapons.droptanks.HB_A6E_AERO1D_EMPTY'
ENUMS.Storage.weapons.droptanks.Drop_tank_75gal = 'weapons.droptanks.Drop tank 75gal'
ENUMS.Storage.weapons.droptanks.S_3_PTB = 'weapons.droptanks.S-3-PTB'
ENUMS.Storage.weapons.droptanks.PTB_3000 = 'weapons.droptanks.PTB-3000'
ENUMS.Storage.weapons.droptanks.HB_A6E_D704 = 'weapons.droptanks.HB_A6E_D704'
ENUMS.Storage.weapons.droptanks.PTB_1150_29 = 'weapons.droptanks.PTB-1150-29'
ENUMS.Storage.weapons.droptanks.f_18c_ptb = 'weapons.droptanks.f-18c-ptb'
ENUMS.Storage.weapons.droptanks.F4_BAK_L = 'weapons.droptanks.F4-BAK-L'
ENUMS.Storage.weapons.droptanks.HB_A6E_AERO1D = 'weapons.droptanks.HB_A6E_AERO1D'
ENUMS.Storage.weapons.droptanks.IAFS_ComboPak_100 = 'weapons.droptanks.{IAFS_ComboPak_100}'
ENUMS.Storage.weapons.droptanks.PTB_2000 = 'weapons.droptanks.PTB-2000'
ENUMS.Storage.weapons.droptanks.M2000_PTB = 'weapons.droptanks.M2000-PTB'
ENUMS.Storage.weapons.droptanks.PTB_150 = 'weapons.droptanks.PTB-150'
ENUMS.Storage.weapons.droptanks.PTB_1150 = 'weapons.droptanks.PTB-1150'
ENUMS.Storage.weapons.droptanks.fuel_tank_300gal = 'weapons.droptanks.fuel_tank_300gal'
ENUMS.Storage.weapons.droptanks.MIG_25_PTB = 'weapons.droptanks.MIG-25-PTB'
ENUMS.Storage.weapons.droptanks.PTB_1500 = 'weapons.droptanks.PTB-1500'
ENUMS.Storage.weapons.droptanks.MIG_23_PTB = 'weapons.droptanks.MIG-23-PTB'
ENUMS.Storage.weapons.droptanks.FT600 = 'weapons.droptanks.FT600'
ENUMS.Storage.weapons.droptanks.F15_PTB = 'weapons.droptanks.F15-PTB'
ENUMS.Storage.weapons.droptanks.ah6_auxtank = 'weapons.droptanks.ah6_auxtank'
ENUMS.Storage.weapons.droptanks.T_PTB = 'weapons.droptanks.T-PTB'
ENUMS.Storage.weapons.droptanks.fuel_tank_370gal = 'weapons.droptanks.fuel_tank_370gal'
ENUMS.Storage.weapons.droptanks.F4_BAK_C = 'weapons.droptanks.F4-BAK-C'
ENUMS.Storage.weapons.droptanks.F16_PTB_N2 = 'weapons.droptanks.F-16-PTB-N2'
ENUMS.Storage.weapons.droptanks.PTB_800 = 'weapons.droptanks.PTB-800'
ENUMS.Storage.weapons.droptanks.PTB_275 = 'weapons.droptanks.PTB-275'
ENUMS.Storage.weapons.droptanks.T_PTB = 'weapons.droptanks.T-PTB'
ENUMS.Storage.weapons.droptanks.F16_PTB_N2 = 'weapons.droptanks.F-16-PTB-N2'
ENUMS.Storage.weapons.droptanks.F4_BAK_C = 'weapons.droptanks.F4-BAK-C'
ENUMS.Storage.weapons.droptanks.PTB_1150_29 = 'weapons.droptanks.PTB-1150-29'
ENUMS.Storage.weapons.droptanks.PTB_1150 = 'weapons.droptanks.PTB-1150'
ENUMS.Storage.weapons.droptanks.fuel_tank_300gal = 'weapons.droptanks.fuel_tank_300gal'
ENUMS.Storage.weapons.droptanks.MIG_25_PTB = 'weapons.droptanks.MIG-25-PTB'
ENUMS.Storage.weapons.droptanks.PTB_1500 = 'weapons.droptanks.PTB-1500'
ENUMS.Storage.weapons.droptanks.FT600 = 'weapons.droptanks.FT600'
ENUMS.Storage.weapons.droptanks.Drop_tank_75gal = 'weapons.droptanks.Drop tank 75gal'
ENUMS.Storage.weapons.droptanks.F15_PTB = 'weapons.droptanks.F15-PTB'
ENUMS.Storage.weapons.droptanks.M2000_PTB = 'weapons.droptanks.M2000-PTB'
ENUMS.Storage.weapons.droptanks.PTB_150 = 'weapons.droptanks.PTB-150'
ENUMS.Storage.weapons.droptanks.F4_BAK_L = 'weapons.droptanks.F4-BAK-L'
ENUMS.Storage.weapons.droptanks.PTB_3000 = 'weapons.droptanks.PTB-3000'
ENUMS.Storage.weapons.droptanks.PTB_2000 = 'weapons.droptanks.PTB-2000'
ENUMS.Storage.weapons.droptanks.S_3_PTB = 'weapons.droptanks.S-3-PTB'
ENUMS.Storage.weapons.droptanks.fuel_tank_370gal = 'weapons.droptanks.fuel_tank_370gal'
ENUMS.Storage.weapons.droptanks.MIG_23_PTB = 'weapons.droptanks.MIG-23-PTB'
ENUMS.Storage.weapons.droptanks.f_18c_ptb = 'weapons.droptanks.f-18c-ptb'
ENUMS.Storage.weapons.containers.FN_HMP400_100 = 'weapons.containers.{FN_HMP400_100}'
ENUMS.Storage.weapons.containers.AN_M3 = 'weapons.containers.{AN-M3}'
ENUMS.Storage.weapons.containers.OH58D_M3P_L500 = 'weapons.containers.OH58D_M3P_L500'
ENUMS.Storage.weapons.containers.GIAT_M621_HE = 'weapons.containers.{GIAT_M621_HE}'
ENUMS.Storage.weapons.containers.KORD_12_7_MI24_L = 'weapons.containers.KORD_12_7_MI24_L'
ENUMS.Storage.weapons.containers.CH47_STBD_M240H = 'weapons.containers.{CH47_STBD_M240H}'
ENUMS.Storage.weapons.containers.CH47_AFT_M60D = 'weapons.containers.{CH47_AFT_M60D}'
ENUMS.Storage.weapons.containers.UPK_23_250_MiG_21 = 'weapons.containers.{UPK-23-250 MiG-21}'
ENUMS.Storage.weapons.containers.CH47_STBD_M134D = 'weapons.containers.{CH47_STBD_M134D}'
ENUMS.Storage.weapons.containers.GAU_12_Equalizer = 'weapons.containers.{GAU_12_Equalizer}'
ENUMS.Storage.weapons.containers.PKT_7_62 = 'weapons.containers.PKT_7_62'
ENUMS.Storage.weapons.containers.CH47_AFT_M240H = 'weapons.containers.{CH47_AFT_M240H}'
ENUMS.Storage.weapons.containers.GIAT_M621_SAPHEI = 'weapons.containers.{GIAT_M621_SAPHEI}'
ENUMS.Storage.weapons.containers.ADEN_GUNPOD = 'weapons.containers.{ADEN_GUNPOD}'
ENUMS.Storage.weapons.containers.GAU_12_Equalizer_HE = 'weapons.containers.{GAU_12_Equalizer_HE}'
ENUMS.Storage.weapons.containers.M60_SIDE_L = 'weapons.containers.M60_SIDE_L'
ENUMS.Storage.weapons.containers.KORD_12_7_MI24_R = 'weapons.containers.KORD_12_7_MI24_R'
ENUMS.Storage.weapons.containers.GIAT_M621_APHE = 'weapons.containers.{GIAT_M621_APHE}'
ENUMS.Storage.weapons.containers.MB339_ANM3_L = 'weapons.containers.{MB339_ANM3_L}'
ENUMS.Storage.weapons.containers.FN_HMP400 = 'weapons.containers.{FN_HMP400}'
ENUMS.Storage.weapons.containers.AH_6_Door = 'weapons.containers.{AH-6_Door}'
ENUMS.Storage.weapons.containers.MB339_DEFA553_L = 'weapons.containers.{MB339_DEFA553_L}'
ENUMS.Storage.weapons.containers.MB339_ANM3_R = 'weapons.containers.{MB339_ANM3_R}'
ENUMS.Storage.weapons.containers.PK_3 = 'weapons.containers.{PK-3}'
ENUMS.Storage.weapons.containers.GUV_VOG = 'weapons.containers.GUV_VOG'
ENUMS.Storage.weapons.containers.SA342_M134_SIDE_R = 'weapons.containers.{SA342_M134_SIDE_R}'
ENUMS.Storage.weapons.containers.OH58D_M3P_L100 = 'weapons.containers.OH58D_M3P_L100'
ENUMS.Storage.weapons.containers.MXU_648 = 'weapons.containers.MXU-648'
ENUMS.Storage.weapons.containers.OH58D_M3P_L400 = 'weapons.containers.OH58D_M3P_L400'
ENUMS.Storage.weapons.containers.FN_HMP400_200 = 'weapons.containers.{FN_HMP400_200}'
ENUMS.Storage.weapons.containers.GIAT_M621_HE = 'weapons.containers.{GIAT_M621_HE}'
ENUMS.Storage.weapons.containers.M134_L = 'weapons.containers.M134_L'
ENUMS.Storage.weapons.containers.OH58D_M3P_L200 = 'weapons.containers.OH58D_M3P_L200'
ENUMS.Storage.weapons.containers.GIAT_M621_AP = 'weapons.containers.{GIAT_M621_AP}'
ENUMS.Storage.weapons.containers.CH47_STBD_M134D = 'weapons.containers.{CH47_STBD_M134D}'
ENUMS.Storage.weapons.containers.PKT_7_62 = 'weapons.containers.PKT_7_62'
ENUMS.Storage.weapons.containers.OH58D_M3P_L300 = 'weapons.containers.OH58D_M3P_L300'
ENUMS.Storage.weapons.containers.GIAT_M621_SAPHEI = 'weapons.containers.{GIAT_M621_SAPHEI}'
ENUMS.Storage.weapons.containers.M60_SIDE_L = 'weapons.containers.M60_SIDE_L'
ENUMS.Storage.weapons.containers.CH47_PORT_M134D = 'weapons.containers.{CH47_PORT_M134D}'
ENUMS.Storage.weapons.containers.CH47_PORT_M60D = 'weapons.containers.{CH47_PORT_M60D}'
ENUMS.Storage.weapons.containers.ADEN_GUNPOD = 'weapons.containers.{ADEN_GUNPOD}'
ENUMS.Storage.weapons.containers.C_101_DEFA553 = 'weapons.containers.{C-101-DEFA553}'
ENUMS.Storage.weapons.containers.MB339_ANM3_R = 'weapons.containers.{MB339_ANM3_R}'
ENUMS.Storage.weapons.containers.CH47_AFT_M60D = 'weapons.containers.{CH47_AFT_M60D}'
ENUMS.Storage.weapons.containers.KORD_12_7 = 'weapons.containers.KORD_12_7'
ENUMS.Storage.weapons.containers.GIAT_M621_HEAP = 'weapons.containers.{GIAT_M621_HEAP}'
ENUMS.Storage.weapons.containers.CH47_AFT_M3M = 'weapons.containers.{CH47_AFT_M3M}'
ENUMS.Storage.weapons.containers.GUV_YakB_GSHP = 'weapons.containers.GUV_YakB_GSHP'
ENUMS.Storage.weapons.containers.R_73U = 'weapons.containers.R-73U'
ENUMS.Storage.weapons.containers.GUV_VOG = 'weapons.containers.GUV_VOG'
ENUMS.Storage.weapons.containers.KORD_12_7_MI24_L = 'weapons.containers.KORD_12_7_MI24_L'
ENUMS.Storage.weapons.containers.OH58D_M3P_L100 = 'weapons.containers.OH58D_M3P_L100'
ENUMS.Storage.weapons.containers.OH58D_M3P_L400 = 'weapons.containers.OH58D_M3P_L400'
ENUMS.Storage.weapons.containers.CH47_STBD_M240H = 'weapons.containers.{CH47_STBD_M240H}'
ENUMS.Storage.weapons.containers.CH47_PORT_M240H = 'weapons.containers.{CH47_PORT_M240H}'
ENUMS.Storage.weapons.containers.CC420_GUN_POD = 'weapons.containers.{CC420_GUN_POD}'
ENUMS.Storage.weapons.containers.FN_HMP400_100 = 'weapons.containers.{FN_HMP400_100}'
ENUMS.Storage.weapons.containers.MB339_ANM3_L = 'weapons.containers.{MB339_ANM3_L}'
ENUMS.Storage.weapons.containers.MB339_DEFA553_R = 'weapons.containers.{MB339_DEFA553_R}'
ENUMS.Storage.weapons.containers.GAU_12_Equalizer_HE = 'weapons.containers.{GAU_12_Equalizer_HE}'
ENUMS.Storage.weapons.containers.OH58D_M3P_L500 = 'weapons.containers.OH58D_M3P_L500'
ENUMS.Storage.weapons.containers.SUU_23_POD = 'weapons.containers.{SUU_23_POD}'
ENUMS.Storage.weapons.containers.PK_3 = 'weapons.containers.{PK-3}'
ENUMS.Storage.weapons.containers.AKAN = 'weapons.containers.{AKAN}'
ENUMS.Storage.weapons.containers.CH47_STBD_M60D = 'weapons.containers.{CH47_STBD_M60D}'
ENUMS.Storage.weapons.containers.SA342_M134_SIDE_R = 'weapons.containers.{SA342_M134_SIDE_R}'
ENUMS.Storage.weapons.containers.M60_SIDE_R = 'weapons.containers.M60_SIDE_R'
ENUMS.Storage.weapons.containers.GAU_12_Equalizer_AP = 'weapons.containers.{GAU_12_Equalizer_AP}'
ENUMS.Storage.weapons.containers.GAU_12_Equalizer = 'weapons.containers.{GAU_12_Equalizer}'
ENUMS.Storage.weapons.containers.KORD_12_7_MI24_R = 'weapons.containers.KORD_12_7_MI24_R'
ENUMS.Storage.weapons.containers.M134_SIDE_R = 'weapons.containers.M134_SIDE_R'
ENUMS.Storage.weapons.containers.AKAN_NO_TRC = 'weapons.containers.{AKAN_NO_TRC}'
ENUMS.Storage.weapons.containers.oh_58_brauning = 'weapons.containers.oh-58-brauning'
ENUMS.Storage.weapons.containers.MXU_648 = 'weapons.containers.MXU-648'
ENUMS.Storage.weapons.containers.M134_R = 'weapons.containers.M134_R'
ENUMS.Storage.weapons.containers.AN_M3 = 'weapons.containers.{AN-M3}'
ENUMS.Storage.weapons.containers.GIAT_M621_APHE = 'weapons.containers.{GIAT_M621_APHE}'
ENUMS.Storage.weapons.containers.AIM_9S = 'weapons.containers.AIM-9S'
ENUMS.Storage.weapons.containers.CH47_AFT_M240H = 'weapons.containers.{CH47_AFT_M240H}'
ENUMS.Storage.weapons.containers.FN_HMP400 = 'weapons.containers.{FN_HMP400}'
ENUMS.Storage.weapons.containers.M134_SIDE_L = 'weapons.containers.M134_SIDE_L'
ENUMS.Storage.weapons.containers.MB339_DEFA553_L = 'weapons.containers.{MB339_DEFA553_L}'
ENUMS.Storage.weapons.containers.MISC_1 = 'weapons.containers.{05544F1A-C39C-466b-BC37-5BD1D52E57BB}'
ENUMS.Storage.weapons.containers.MISC_2 = 'weapons.containers.{E92CBFE5-C153-11d8-9897-000476191836}'
ENUMS.Storage.weapons.containers.hvar_SmokeGenerator = 'weapons.containers.hvar_SmokeGenerator'
ENUMS.Storage.weapons.containers.INV_SMOKE_RED = 'weapons.containers.{INV-SMOKE-RED}'
ENUMS.Storage.weapons.containers.INV_SMOKE_YELLOW = 'weapons.containers.{INV-SMOKE-YELLOW}'
ENUMS.Storage.weapons.containers.INV_SMOKE_BLUE = 'weapons.containers.{INV-SMOKE-BLUE}'
ENUMS.Storage.weapons.containers.INV_SMOKE_GREEN = 'weapons.containers.{INV-SMOKE-GREEN}'
ENUMS.Storage.weapons.containers.INV_SMOKE_WHITE = 'weapons.containers.{INV-SMOKE-WHITE}'
ENUMS.Storage.weapons.containers.INV_SMOKE_ORANGE = 'weapons.containers.{INV-SMOKE-ORANGE}'
ENUMS.Storage.weapons.containers.GAU_12_Equalizer_AP = 'weapons.containers.{GAU_12_Equalizer_AP}'
ENUMS.Storage.weapons.containers.AH_6_Gunners = 'weapons.containers.{AH-6_Gunners}'
ENUMS.Storage.weapons.containers.AH_6_FN_HMP400 = 'weapons.containers.{AH-6_FN_HMP400}'
ENUMS.Storage.weapons.containers.R_73U = 'weapons.containers.R-73U'
ENUMS.Storage.weapons.containers.M60_SIDE_R = 'weapons.containers.M60_SIDE_R'
ENUMS.Storage.weapons.containers.CH47_AFT_M3M = 'weapons.containers.{CH47_AFT_M3M}'
ENUMS.Storage.weapons.containers.GUV_YakB_GSHP = 'weapons.containers.GUV_YakB_GSHP'
ENUMS.Storage.weapons.containers.RKL609_L = 'weapons.containers.{RKL609_L}'
ENUMS.Storage.weapons.containers.CH47_PORT_M134D = 'weapons.containers.{CH47_PORT_M134D}'
ENUMS.Storage.weapons.containers.CH47_PORT_M60D = 'weapons.containers.{CH47_PORT_M60D}'
ENUMS.Storage.weapons.containers.RKL609_R = 'weapons.containers.{RKL609_R}'
ENUMS.Storage.weapons.containers.C_101_DEFA553 = 'weapons.containers.{C-101-DEFA553}'
ENUMS.Storage.weapons.containers.AH6_M134L = 'weapons.containers.{AH6_M134L}'
ENUMS.Storage.weapons.containers.KORD_12_7 = 'weapons.containers.KORD_12_7'
ENUMS.Storage.weapons.containers.C130_M18_Sidearm = 'weapons.containers.{C130-M18-Sidearm}'
ENUMS.Storage.weapons.containers.GIAT_M621_HEAP = 'weapons.containers.{GIAT_M621_HEAP}'
ENUMS.Storage.weapons.containers.BRU_42_LS = 'weapons.containers.BRU-42_LS'
ENUMS.Storage.weapons.containers.CH47_STBD_M60D = 'weapons.containers.{CH47_STBD_M60D}'
ENUMS.Storage.weapons.containers.SUU_23_POD = 'weapons.containers.{SUU_23_POD}'
ENUMS.Storage.weapons.containers.M134_SIDE_R = 'weapons.containers.M134_SIDE_R'
ENUMS.Storage.weapons.containers.AKAN_NO_TRC = 'weapons.containers.{AKAN_NO_TRC}'
ENUMS.Storage.weapons.containers.oh_58_brauning = 'weapons.containers.oh-58-brauning'
ENUMS.Storage.weapons.containers.AH_6_DOORS = 'weapons.containers.{AH-6_DOORS}'
ENUMS.Storage.weapons.containers.CC420_GUN_POD = 'weapons.containers.{CC420_GUN_POD}'
ENUMS.Storage.weapons.containers.CH47_PORT_M240H = 'weapons.containers.{CH47_PORT_M240H}'
ENUMS.Storage.weapons.containers.MB339_DEFA553_R = 'weapons.containers.{MB339_DEFA553_R}'
ENUMS.Storage.weapons.containers.AIM_9S = 'weapons.containers.AIM-9S'
ENUMS.Storage.weapons.containers.hvar_SmokeGenerator = 'weapons.containers.hvar_SmokeGenerator'
ENUMS.Storage.weapons.containers.M134_L = 'weapons.containers.M134_L'
ENUMS.Storage.weapons.containers.AKAN = 'weapons.containers.{AKAN}'
ENUMS.Storage.weapons.containers.C130_Cargo_Bay_M4 = 'weapons.containers.{C130-Cargo-Bay-M4}'
ENUMS.Storage.weapons.containers.M134_SIDE_L = 'weapons.containers.M134_SIDE_L'
ENUMS.Storage.weapons.containers.FN_HMP400_200 = 'weapons.containers.{FN_HMP400_200}'
ENUMS.Storage.weapons.containers.OH58D_M3P_L200 = 'weapons.containers.OH58D_M3P_L200'
ENUMS.Storage.weapons.containers.GIAT_M621_AP = 'weapons.containers.{GIAT_M621_AP}'
ENUMS.Storage.weapons.containers.M134_R = 'weapons.containers.M134_R'
ENUMS.Storage.weapons.containers.OH58D_M3P_L300 = 'weapons.containers.OH58D_M3P_L300'
ENUMS.Storage.weapons.containers.AH6_M134R = 'weapons.containers.{AH6_M134R}'
ENUMS.Storage.weapons.torpedoes.G7A_T1 = 'weapons.torpedoes.G7A_T1'
-- UH-60L Mod
ENUMS.Storage.weapons.gunmounts.UH60LGAU19 = 'weapons.gunmounts.UH-60L GAU-19'
@@ -1788,6 +2081,15 @@ ENUMS.Storage.weapons.gunmounts.UH60L_M60_GUNNER = 'weapons.gunmounts.{UH60L_M60
ENUMS.Storage.weapons.gunmounts.UH60L_M2_GUNNER = 'weapons.gunmounts.{UH60L_M2_GUNNER}'
ENUMS.Storage.weapons.gunmounts.UH60_M230_LEFT = 'weapons.gunmounts.{UH60_M230_LEFT}'
ENUMS.Storage.weapons.gunmounts.UH60_M230_RIGHT = 'weapons.gunmounts.{UH60_M230_RIGHT}'
ENUMS.Storage.weapons.containers.UH60_M134_RIGHT = 'weapons.containers.{UH60_M134_RIGHT}'
ENUMS.Storage.weapons.containers.UH60_M134_LEFT = 'weapons.containers.{UH60_M134_LEFT}'
ENUMS.Storage.weapons.containers.UH60_M230_LEFT = 'weapons.containers.{UH60_M230_LEFT}'
ENUMS.Storage.weapons.containers.UH60L_M134_GUNNER = 'weapons.containers.{UH60L_M134_GUNNER}'
ENUMS.Storage.weapons.containers.UH60_GAU19_LEFT = 'weapons.containers.{UH60_GAU19_LEFT}'
ENUMS.Storage.weapons.containers.UH60L_M60_GUNNER = 'weapons.containers.{UH60L_M60_GUNNER}'
ENUMS.Storage.weapons.containers.UH60_GAU19_RIGHT = 'weapons.containers.{UH60_GAU19_RIGHT}'
ENUMS.Storage.weapons.containers.UH60_M230_RIGHT = 'weapons.containers.{UH60_M230_RIGHT}'
ENUMS.Storage.weapons.containers.UH60L_M2_GUNNER = 'weapons.containers.{UH60L_M2_GUNNER}'
---
-- @type ENUMS.FARPType
+358 -27
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.
@@ -1491,6 +1507,14 @@ function UTILS.VecSubstract(a, b)
return {x=a.x-b.x, y=a.y-b.y, z=a.z-b.z}
end
--- Scale a 3D vectors by multiplication with s.
-- @param DCS#Vec3 v A Vector in 3D with x, y, z components.
-- @param #number s Scale
-- @return DCS#Vec3 Vec3
function UTILS.VecScale(v, s)
return {x = v.x * s, y = v.y * s, z = v.z * s}
end
--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.VecSubtract(a, b)
return UTILS.VecSubstract(a, b)
@@ -1504,6 +1528,14 @@ function UTILS.Vec2Substract(a, b)
return {x=a.x-b.x, y=a.y-b.y}
end
--- Calculate the difference between two 3D vectors by substracting the x,y,z components from each other.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return DCS#Vec3 Vector in 3D.
function UTILS.Vec3Substract(a, b)
return {x = a.x - b.x, y = a.y - b.y, z = a.z - b.z}
end
--- Substract is not a word, don't want to rename the original function because it's been around since forever
function UTILS.Vec2Subtract(a, b)
return UTILS.Vec2Substract(a, b)
@@ -1633,6 +1665,13 @@ function UTILS.Vec2Translate(a, distance, angle)
return {x=TX, y=TY}
end
--- Calculate the lenght of a 3D vector
-- @param DCS#Vec3 v A Vector in 3D with x, y, z components.
-- @return #number length
function UTILS.Vec3Length(v)
return math.sqrt(v.x * v.x + v.y * v.y + v.z * v.z)
end
--- Rotate 3D vector in the 2D (x,z) plane. y-component (usually altitude) unchanged.
-- @param DCS#Vec3 a Vector in 3D with x, y, z components.
-- @param #number angle Rotation angle in degrees.
@@ -2014,6 +2053,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 +2345,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
@@ -2863,16 +2923,27 @@ end
-- @param #boolean Cinematic (Optional, needs Structured = true) If true, place a fire/smoke effect on the dead static position.
-- @param #number Effect (Optional for Cinematic) What effect to use. Defaults to a random effect. Smoke presets are: 1=small smoke and fire, 2=medium smoke and fire, 3=large smoke and fire, 4=huge smoke and fire, 5=small smoke, 6=medium smoke, 7=large smoke, 8=huge smoke.
-- @param #number Density (Optional for Cinematic) What smoke density to use, can be 0 to 1. Defaults to 0.5.
-- @param #boolean Resurrection If true, dead units can be restored on load. Defaults to false.
-- @param #number ResurrectPercentage Use this percentage of probability to resurrect a unit. [0..100], defaults to 25.
-- @param #number Healmin If set, life points of a resurrected unit will be randomly restored to min this percentage. [0..100], defaults to 25.
-- @param #number Healmax If set, life points of a resurrected unit will be randomly restored to max this percentage. [0..100], defaults to 75.
-- @return #table Table of data objects (tables) containing groupname, coordinate and group object. Returns nil when file cannot be read.
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density)
function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinematic,Effect,Density,Resurrection,ResurrectPercentage,Healmin,Healmax)
local healmin = Healmin or 25
local healmax = Healmax or 75
local resurrection = (Resurrection == true) and true or false
local resurrectpercentage = ResurrectPercentage or 25
local fires = {}
---
-- @param Core.Point#COORDINATE coord
local function Smokers(name,coord,effect,density)
local eff = math.random(8)
if type(effect) == "number" then eff = effect end
coord:BigSmokeAndFire(eff,density,name)
coord:BigSmokeAndFire( eff, Density, 300, 1, name )
table.insert(fires,name)
end
@@ -2887,7 +2958,16 @@ function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinema
local name = _unit:GetName()
Smokers(name,coordinate,Effect,Density)
end
_unit:Destroy(false)
-- TODO Resurrection logic
local resurectok = math.random(1,100)
BASE:E(string.format("Load Group | Resurrection | Resurrect %s | Thresh %d | Random %d",tostring(resurrection),resurrectpercentage,resurectok))
if resurrection == true and (resurectok < resurrectpercentage) then
local heallife = math.random(healmin,healmax)
BASE:E("Load Group | Resurrection | Life "..heallife)
_unit:SetLife(heallife)
else
_unit:Destroy(false)
end
reduced = reduced + 1
if reduced == anzahl then break end
end
@@ -4222,25 +4302,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 +4401,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 +4408,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"},
@@ -4537,6 +4618,18 @@ function UTILS.Vec2toVec3(vec,y)
end
end
-- Converts a Vec3 to a Vec2
-- @param vec3 the Vec3 to convert
-- @return Vec2 The Vec2 output
function UTILS.Vec3toVec2(Vec3)
if Vec3 and type(Vec3)=="table" then
local Vec2 = {}
Vec2.x = Vec3.x or 0
Vec2.y = Vec3.z or 0
return Vec2
end
end
--- Get the correction needed for true north in radians
-- @param gPoint The map point vec2 or vec3
-- @return number correction
@@ -5274,3 +5367,241 @@ 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
--- Calculated optimal intercepting course (Bearing)
-- @param DCS#Vec3 A1 Position of flight one
-- @param DCS#Vec3 V1 VelocityVector of flight one
-- @param DCS#Vec3 A2 Position of flight two
-- @param #number V2_speed Speed of A2 in m/s
-- @return #number Bearing Bearing course for A2 to take for an intercept of A1 or nil if aspect is hot/cold.
function UTILS.CalculateInterceptBearing(A1, V1, A2, V2_speed)
local function berechne_bearing(richtung)
local bearing = math.deg(math.atan2(richtung.x, richtung.y))
if bearing < 0 then
bearing = bearing + 360
end
return bearing
end
local function vec_normalize(v)
local len = UTILS.Vec3Length(v)
if len == 0 then return {x = 0, y = 0, z = 0} end
return {x = v.x / len, y = v.y / len, z = v.z / len}
end
-- Relative Position von F1 zu F2
local rel_pos = UTILS.Vec3Substract(A1, A2)
local distance = UTILS.Vec3Length(rel_pos)
if distance == 0 then
return nil -- Bereits am gleichen Ort
end
-- Richtungsvektor von F2 zu F1 (normalisiert)
local richtung_zu_f1 = vec_normalize(rel_pos)
-- Prüfe HOT: F1 fliegt direkt auf F2 zu
-- Das ist der Fall wenn V1 in die entgegengesetzte Richtung von rel_pos zeigt
local v1_normalisiert = vec_normalize(V1)
local annaeherung = UTILS.VecDot(v1_normalisiert, richtung_zu_f1)
if annaeherung < -0.95 then -- F1 fliegt fast direkt auf F2 zu (Winkel > ~162°)
return nil -- Hot
end
-- Prüfe COLD: F1 fliegt parallel weg von F2
-- Berechne die Geschwindigkeitsdifferenz
local rel_velocity = UTILS.VecSubstract(V1, {x=0, y=0, z=0}) -- V1 relativ zu F2 (falls F2 stillsteht)
local flucht_komponente = UTILS.VecDot(vec_normalize(rel_velocity), richtung_zu_f1)
if flucht_komponente > 0.95 then -- F1 fliegt fast parallel weg (Winkel < ~18°)
return nil -- Cold
end
-- Geschwindigkeiten
local v1 = UTILS.Vec3Length(V1)
local v2 = V2_speed
-- Löse quadratische Gleichung für Abfangzeit t
local a = UTILS.VecDot(V1, V1) - v2 * v2
local b = 2 * UTILS.VecDot(rel_pos, V1)
local c = UTILS.VecDot(rel_pos, rel_pos)
local discriminant = b * b - 4 * a * c
if discriminant < 0 then
return nil -- Keine Lösung möglich
end
-- Wähle die positive, kleinste Lösung
local t1 = (-b + math.sqrt(discriminant)) / (2 * a)
local t2 = (-b - math.sqrt(discriminant)) / (2 * a)
local t = nil
if t1 > 0 and t2 > 0 then
t = math.min(t1, t2)
elseif t1 > 0 then
t = t1
elseif t2 > 0 then
t = t2
else
return nil -- Keine positive Lösung
end
-- Berechne Treffpunkt
local treffpunkt = UTILS.VecAdd(A1, UTILS.VecScale(V1, t))
-- Berechne Richtung zum Treffpunkt
local richtung = UTILS.VecSubstract(treffpunkt, A2)
-- Berechne Bearing
local bearing = berechne_bearing(richtung)
return UTILS.Round(bearing,0)
end
+181 -67
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.
@@ -717,62 +718,62 @@ AIRBASE.SouthAtlantic = {
--- Airbases of the Sinai map
--
-- * `AIRBASE.SinaiMap.Abu_Rudeis` Abu Rudeis
-- * `AIRBASE.SinaiMap.Abu_Suwayr` Abu Suwayr
-- * `AIRBASE.SinaiMap.Al_Bahr_al_Ahmar` Al Bahr al Ahmar
-- * `AIRBASE.SinaiMap.Al_Ismailiyah` Al Ismailiyah
-- * `AIRBASE.SinaiMap.Al_Khatatbah` Al Khatatbah
-- * `AIRBASE.SinaiMap.Al_Mansurah` Al Mansurah
-- * `AIRBASE.SinaiMap.Al_Rahmaniyah_Air_Base` Al Rahmaniyah Air Base
-- * `AIRBASE.SinaiMap.As_Salihiyah` As Salihiyah
-- * `AIRBASE.SinaiMap.AzZaqaziq` AzZaqaziq
-- * `AIRBASE.SinaiMap.Baluza` Baluza
-- * `AIRBASE.SinaiMap.Ben_Gurion` Ben-Gurion
-- * `AIRBASE.SinaiMap.Beni_Suef` Beni Suef
-- * `AIRBASE.SinaiMap.Bilbeis_Air_Base` Bilbeis Air Base
-- * `AIRBASE.SinaiMap.Bir_Hasanah` Bir Hasanah
-- * `AIRBASE.SinaiMap.Birma_Air_Base` Birma Air Base
-- * `AIRBASE.SinaiMap.Borg_El_Arab_International_Airport` Borg El Arab International Airport
-- * `AIRBASE.SinaiMap.Cairo_International_Airport` Cairo International Airport
-- * `AIRBASE.SinaiMap.Cairo_West` Cairo West
-- * `AIRBASE.SinaiMap.Damascus_Intl` Damascus Intl
-- * `AIRBASE.SinaiMap.Difarsuwar_Airfield` Difarsuwar Airfield
-- * `AIRBASE.SinaiMap.Ein_Shamer` Ein Shamer
-- * `AIRBASE.SinaiMap.El_Arish` El Arish
-- * `AIRBASE.SinaiMap.El_Gora` El Gora
-- * `AIRBASE.SinaiMap.El_Minya` El Minya
-- * `AIRBASE.SinaiMap.Fayed` Fayed
-- * `AIRBASE.SinaiMap.Gebel_El_Basur_Air_Base` Gebel El Basur Air Base
-- * `AIRBASE.SinaiMap.Hatzerim` Hatzerim
-- * `AIRBASE.SinaiMap.Hatzor` Hatzor
-- * `AIRBASE.SinaiMap.Hurghada_International_Airport` Hurghada International Airport
-- * `AIRBASE.SinaiMap.Inshas_Airbase` Inshas Airbase
-- * `AIRBASE.SinaiMap.Jiyanklis_Air_Base` Jiyanklis Air Base
-- * `AIRBASE.SinaiMap.Kedem` Kedem
-- * `AIRBASE.SinaiMap.Khalkhalah_Air_Base` Khalkhalah Air Base
-- * `AIRBASE.SinaiMap.Kibrit_Air_Base` Kibrit Air Base
-- * `AIRBASE.SinaiMap.King_Feisal_Air_Base` King Feisal Air Base
-- * `AIRBASE.SinaiMap.Kom_Awshim` Kom Awshim
-- * `AIRBASE.SinaiMap.Megiddo` Megiddo
-- * `AIRBASE.SinaiMap.Melez` Melez
-- * `AIRBASE.SinaiMap.Mezzeh_Air_Base` Mezzeh Air Base
-- * `AIRBASE.SinaiMap.Nevatim` Nevatim
-- * `AIRBASE.SinaiMap.Ovda` Ovda
-- * `AIRBASE.SinaiMap.Palmachim` Palmachim
-- * `AIRBASE.SinaiMap.Quwaysina` Quwaysina
-- * `AIRBASE.SinaiMap.Rafic_Hariri_Intl` Rafic Hariri Intl
-- * `AIRBASE.SinaiMap.Ramat_David` Ramat David
-- * `AIRBASE.SinaiMap.Ramon_Airbase` Ramon Airbase
-- * `AIRBASE.SinaiMap.Ramon_International_Airport` Ramon International Airport
-- * `AIRBASE.SinaiMap.Sde_Dov` Sde Dov
-- * `AIRBASE.SinaiMap.Sharm_El_Sheikh_International_Airport` Sharm El Sheikh International Airport
-- * `AIRBASE.SinaiMap.St_Catherine` St Catherine
-- * `AIRBASE.SinaiMap.Taba_International_Airport` Taba International Airport
-- * `AIRBASE.SinaiMap.Tabuk` Tabuk
-- * `AIRBASE.SinaiMap.TabukHeliBase` TabukHeliBase
-- * `AIRBASE.SinaiMap.Tel_Nof` Tel Nof
-- * `AIRBASE.SinaiMap.Wadi_Abu_Rish` Wadi Abu Rish
-- * `AIRBASE.SinaiMap.Wadi_al_Jandali` Wadi al Jandali
-- * `AIRBASE.Sinai.Abu_Rudeis` Abu Rudeis
-- * `AIRBASE.Sinai.Abu_Suwayr` Abu Suwayr
-- * `AIRBASE.Sinai.Al_Bahr_al_Ahmar` Al Bahr al Ahmar
-- * `AIRBASE.Sinai.Al_Ismailiyah` Al Ismailiyah
-- * `AIRBASE.Sinai.Al_Khatatbah` Al Khatatbah
-- * `AIRBASE.Sinai.Al_Mansurah` Al Mansurah
-- * `AIRBASE.Sinai.Al_Rahmaniyah_Air_Base` Al Rahmaniyah Air Base
-- * `AIRBASE.Sinai.As_Salihiyah` As Salihiyah
-- * `AIRBASE.Sinai.AzZaqaziq` AzZaqaziq
-- * `AIRBASE.Sinai.Baluza` Baluza
-- * `AIRBASE.Sinai.Ben_Gurion` Ben-Gurion
-- * `AIRBASE.Sinai.Beni_Suef` Beni Suef
-- * `AIRBASE.Sinai.Bilbeis_Air_Base` Bilbeis Air Base
-- * `AIRBASE.Sinai.Bir_Hasanah` Bir Hasanah
-- * `AIRBASE.Sinai.Birma_Air_Base` Birma Air Base
-- * `AIRBASE.Sinai.Borg_El_Arab_International_Airport` Borg El Arab International Airport
-- * `AIRBASE.Sinai.Cairo_International_Airport` Cairo International Airport
-- * `AIRBASE.Sinai.Cairo_West` Cairo West
-- * `AIRBASE.Sinai.Damascus_Intl` Damascus Intl
-- * `AIRBASE.Sinai.Difarsuwar_Airfield` Difarsuwar Airfield
-- * `AIRBASE.Sinai.Ein_Shamer` Ein Shamer
-- * `AIRBASE.Sinai.El_Arish` El Arish
-- * `AIRBASE.Sinai.El_Gora` El Gora
-- * `AIRBASE.Sinai.El_Minya` El Minya
-- * `AIRBASE.Sinai.Fayed` Fayed
-- * `AIRBASE.Sinai.Gebel_El_Basur_Air_Base` Gebel El Basur Air Base
-- * `AIRBASE.Sinai.Hatzerim` Hatzerim
-- * `AIRBASE.Sinai.Hatzor` Hatzor
-- * `AIRBASE.Sinai.Hurghada_International_Airport` Hurghada International Airport
-- * `AIRBASE.Sinai.Inshas_Airbase` Inshas Airbase
-- * `AIRBASE.Sinai.Jiyanklis_Air_Base` Jiyanklis Air Base
-- * `AIRBASE.Sinai.Kedem` Kedem
-- * `AIRBASE.Sinai.Khalkhalah_Air_Base` Khalkhalah Air Base
-- * `AIRBASE.Sinai.Kibrit_Air_Base` Kibrit Air Base
-- * `AIRBASE.Sinai.King_Feisal_Air_Base` King Feisal Air Base
-- * `AIRBASE.Sinai.Kom_Awshim` Kom Awshim
-- * `AIRBASE.Sinai.Megiddo` Megiddo
-- * `AIRBASE.Sinai.Melez` Melez
-- * `AIRBASE.Sinai.Mezzeh_Air_Base` Mezzeh Air Base
-- * `AIRBASE.Sinai.Nevatim` Nevatim
-- * `AIRBASE.Sinai.Ovda` Ovda
-- * `AIRBASE.Sinai.Palmachim` Palmachim
-- * `AIRBASE.Sinai.Quwaysina` Quwaysina
-- * `AIRBASE.Sinai.Rafic_Hariri_Intl` Rafic Hariri Intl
-- * `AIRBASE.Sinai.Ramat_David` Ramat David
-- * `AIRBASE.Sinai.Ramon_Airbase` Ramon Airbase
-- * `AIRBASE.Sinai.Ramon_International_Airport` Ramon International Airport
-- * `AIRBASE.Sinai.Sde_Dov` Sde Dov
-- * `AIRBASE.Sinai.Sharm_El_Sheikh_International_Airport` Sharm El Sheikh International Airport
-- * `AIRBASE.Sinai.St_Catherine` St Catherine
-- * `AIRBASE.Sinai.Taba_International_Airport` Taba International Airport
-- * `AIRBASE.Sinai.Tabuk` Tabuk
-- * `AIRBASE.Sinai.TabukHeliBase` TabukHeliBase
-- * `AIRBASE.Sinai.Tel_Nof` Tel Nof
-- * `AIRBASE.Sinai.Wadi_Abu_Rish` Wadi Abu Rish
-- * `AIRBASE.Sinai.Wadi_al_Jandali` Wadi al Jandali
--
-- @field Sinai
AIRBASE.Sinai = {
@@ -833,6 +834,66 @@ AIRBASE.Sinai = {
["Wadi_Abu_Rish"] = "Wadi Abu Rish",
["Wadi_al_Jandali"] = "Wadi al Jandali",
}
---
-- @field SinaiMap
AIRBASE.SinaiMap = {
["Abu_Rudeis"] = "Abu Rudeis",
["Abu_Suwayr"] = "Abu Suwayr",
["Al_Bahr_al_Ahmar"] = "Al Bahr al Ahmar",
["Al_Ismailiyah"] = "Al Ismailiyah",
["Al_Khatatbah"] = "Al Khatatbah",
["Al_Mansurah"] = "Al Mansurah",
["Al_Rahmaniyah_Air_Base"] = "Al Rahmaniyah Air Base",
["As_Salihiyah"] = "As Salihiyah",
["AzZaqaziq"] = "AzZaqaziq",
["Baluza"] = "Baluza",
["Ben_Gurion"] = "Ben-Gurion",
["Beni_Suef"] = "Beni Suef",
["Bilbeis_Air_Base"] = "Bilbeis Air Base",
["Bir_Hasanah"] = "Bir Hasanah",
["Birma_Air_Base"] = "Birma Air Base",
["Borg_El_Arab_International_Airport"] = "Borg El Arab International Airport",
["Cairo_International_Airport"] = "Cairo International Airport",
["Cairo_West"] = "Cairo West",
["Damascus_Intl"] = "Damascus Intl",
["Difarsuwar_Airfield"] = "Difarsuwar Airfield",
["Ein_Shamer"] = "Ein Shamer",
["El_Arish"] = "El Arish",
["El_Gora"] = "El Gora",
["El_Minya"] = "El Minya",
["Fayed"] = "Fayed",
["Gebel_El_Basur_Air_Base"] = "Gebel El Basur Air Base",
["Hatzerim"] = "Hatzerim",
["Hatzor"] = "Hatzor",
["Hurghada_International_Airport"] = "Hurghada International Airport",
["Inshas_Airbase"] = "Inshas Airbase",
["Jiyanklis_Air_Base"] = "Jiyanklis Air Base",
["Kedem"] = "Kedem",
["Khalkhalah_Air_Base"] = "Khalkhalah Air Base",
["Kibrit_Air_Base"] = "Kibrit Air Base",
["King_Feisal_Air_Base"] = "King Feisal Air Base",
["Kom_Awshim"] = "Kom Awshim",
["Megiddo"] = "Megiddo",
["Melez"] = "Melez",
["Mezzeh_Air_Base"] = "Mezzeh Air Base",
["Nevatim"] = "Nevatim",
["Ovda"] = "Ovda",
["Palmachim"] = "Palmachim",
["Quwaysina"] = "Quwaysina",
["Rafic_Hariri_Intl"] = "Rafic Hariri Intl",
["Ramat_David"] = "Ramat David",
["Ramon_Airbase"] = "Ramon Airbase",
["Ramon_International_Airport"] = "Ramon International Airport",
["Sde_Dov"] = "Sde Dov",
["Sharm_El_Sheikh_International_Airport"] = "Sharm El Sheikh International Airport",
["St_Catherine"] = "St Catherine",
["Taba_International_Airport"] = "Taba International Airport",
["Tabuk"] = "Tabuk",
["TabukHeliBase"] = "TabukHeliBase",
["Tel_Nof"] = "Tel Nof",
["Wadi_Abu_Rish"] = "Wadi Abu Rish",
["Wadi_al_Jandali"] = "Wadi al Jandali",
}
--- Airbases of the Kola map
--
@@ -1552,7 +1613,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 +1723,10 @@ end
else
self:E(string.format("ERROR: Cound not get position Vec2 of airbase %s", AirbaseName))
end
if Nrunways>0 then
self:GetMinimumBoundingCircleFromParkingSpots()
end
-- Debug info.
self:T2(string.format("Registered airbase %s", tostring(self.AirbaseName)))
@@ -2104,7 +2169,7 @@ end
function AIRBASE:GetParkingSpotsNumber(termtype)
-- Get free parking spots data.
local parkingdata=self:GetParkingData(false)
local parkingdata=self:GetParkingData(false) or {}
local nspots=0
for _,parkingspot in pairs(parkingdata) do
@@ -2124,7 +2189,7 @@ end
function AIRBASE:GetFreeParkingSpotsNumber(termtype, allowTOAC)
-- Get free parking spots data.
local parkingdata=self:GetParkingData(true)
local parkingdata=self:GetParkingData(true) or {}
local nfree=0
for _,parkingspot in pairs(parkingdata) do
@@ -2147,7 +2212,7 @@ end
function AIRBASE:GetFreeParkingSpotsCoordinates(termtype, allowTOAC)
-- Get free parking spots data.
local parkingdata=self:GetParkingData(true)
local parkingdata=self:GetParkingData(true) or {}
-- Put coordinates of free spots into table.
local spots={}
@@ -2191,13 +2256,62 @@ 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()
if #spots == 0 then return self.AirbaseZone end
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
function AIRBASE:_InitParkingSpots()
-- Get parking data of all spots (free or occupied)
local parkingdata=self:GetParkingData(false)
local parkingdata=self:GetParkingData(false) or {}
-- Init table.
self.parking={}
@@ -2273,10 +2387,10 @@ end
function AIRBASE:GetParkingSpotsTable(termtype)
-- Get parking data of all spots (free or occupied)
local parkingdata=self:GetParkingData(false)
local parkingdata=self:GetParkingData(false) or {}
-- Get parking data of all free spots.
local parkingfree=self:GetParkingData(true)
local parkingfree=self:GetParkingData(true) or {}
-- Function to ckeck if any parking spot is free.
local function _isfree(_tocheck)
@@ -2325,7 +2439,7 @@ end
function AIRBASE:GetFreeParkingSpotsTable(termtype, allowTOAC)
-- Get parking data of all free spots.
local parkingfree=self:GetParkingData(true)
local parkingfree=self:GetParkingData(true) or {}
-- Put coordinates of free spots into table.
local freespots={}
@@ -2355,7 +2469,7 @@ end
function AIRBASE:GetParkingSpotData(TerminalID)
-- Get parking data.
local parkingdata=self:GetParkingSpotsTable()
local parkingdata=self:GetParkingSpotsTable() or {}
for _,_spot in pairs(parkingdata) do
local spot=_spot --#AIRBASE.ParkingSpot
@@ -2381,7 +2495,7 @@ function AIRBASE:MarkParkingSpots(termtype, mark)
end
-- Get parking data from getParking() wrapper function.
local parkingdata=self:GetParkingSpotsTable(termtype)
local parkingdata=self:GetParkingSpotsTable(termtype) or {}
-- Get airbase name.
local airbasename=self:GetName()
@@ -2496,7 +2610,7 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius,
local markobstacles=false
-- Loop over all known parking spots
for _,parkingspot in pairs(parkingdata) do
for _,parkingspot in pairs(parkingdata or {}) do
-- Coordinate of the parking spot.
local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE
+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 }
+25 -6
View File
@@ -1352,9 +1352,7 @@ function UNIT:GetThreatLevel()
end
ThreatText = ThreatLevels[ThreatLevel + 1]
end
if self:IsAir() then
elseif self:IsAir() then
local ThreatLevels = {
[1] = "Unarmed",
@@ -1397,9 +1395,7 @@ function UNIT:GetThreatLevel()
end
ThreatText = ThreatLevels[ThreatLevel + 1]
end
if self:IsShip() then
elseif self:IsShip() then
--["Aircraft Carriers"] = {"Heavy armed ships",},
--["Cruisers"] = {"Heavy armed ships",},
@@ -1953,3 +1949,26 @@ end
function UNIT:SetValidateAndRepositionGroundUnits(Enabled)
self.ValidateAndRepositionGroundUnits = Enabled
end
--- Get the max kgs of fuel this unit can hold in its *internal* tank(s) and overall (with external tanks) in kgs.
-- @param #UNIT self
-- @return #number InternalFuel Max internal fuel in kgs. Zero if it cannot be determined.
-- @return #number Overall Fuel Overall max in case there are external tanks in kgs. Zero if it cannot be determined.
function UNIT:GetFuelMassMax()
local Desc = self:GetDesc() or {}
local massFuelMax=Desc.fuelMassMax or 0
local relFuel=math.min(self:GetFuel() or 1.0, 1.0) -- We take 1.0 as max in case of external fuel tanks.
local massFuel=massFuelMax*relFuel
return massFuel, massFuelMax
end
--- Get the current kgs of fuel this unit holds in all of its tanks.
-- @param #UNIT self
-- @param #number Filling Fuel in kgs.
function UNIT:GetCurrentFuelKgs()
local fuel, maxfuel = self:GetFuelMassMax()
local relfuel = self:GetFuel()
local maxfilling = math.max(fuel,maxfuel)
local mass = maxfilling*relfuel
return mass
end
+1 -1
View File
@@ -350,7 +350,7 @@ end
-- myweapon:SetFuncImpact(OnImpact)
--
-- -- Start tracking.
-- myweapon:Track()
-- myweapon:StartTrack()
--
function WEAPON:SetFuncImpact(FuncImpact, ...)
self.impactFunc=FuncImpact
+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