2 Commits

Author SHA1 Message Date
Ambroise Garel 0e323f7c39 Added function protoypes for JTAC lasing 2025-09-16 20:11:52 +02:00
Ambroise Garel d62557da89 Added "Designate target with laser" menu commands 2025-09-16 19:59:20 +02:00
28 changed files with 159 additions and 665 deletions
+1 -2
View File
@@ -11,9 +11,8 @@
if not net then net = {} end if not net then net = {} end
net.allow_unsafe_api = { -- this defines the secure zones where net.dostring_in() can be called from net.allow_unsafe_api = { -- this defines the secure zones where net.dostring_in() can be called from
"scripting",
} }
net.allow_dostring_in = { -- and this defines the zones that should be addressed from net.dostring_in() net.allow_dostring_in = { -- and this defines the zones that should be addressed from net.dostring_in()
"mission", "mission"
} }
-51
View File
@@ -1,51 +0,0 @@
-- do
-- Library.factions.tables["NATO"] = {}
-- Library.factions.tables["NATO"].theaters = {}
-- Library.factions.tables["NATO"].timePeriods = {}
-- Library.factions.tables["NATO"].units = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.WORLD_WAR_2] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.KOREA_WAR] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.VIETNAM_WAR] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.COLD_WAR] = {}
-- Library.factions.tables["NATO"].units[DCSEx.enums.timePeriod.MODERN] = {
-- [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = { "Gepard", "Vulcan" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = { "Soldier stinger" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*Patriot" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*HAWK", "*NASAMS" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "rapier_fsa", "Roland ADS" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = { "M6 Linebacker", "M48 Chaparral", "M1097 Avenger" },
-- [DCSEx.enums.unitFamily.GROUND_APC] = { "AAV7", "Cobra", "LAV-25", "M-2 Bradley", "M-113", "M1045 HMMWV TOW", "M1126 Stryker ICV", "M1128 Stryker MGS", "Marder", "MCV-80", "MLRS FDDM", "TPZ", "CHAP_M1130", "CHAP_MATV" },
-- [DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "M-109", "MLRS", "CHAP_M142_ATACMS_M48", "CHAP_M142_GMLRS_M31" },
-- [DCSEx.enums.unitFamily.GROUND_INFANTRY] = { "Soldier M4 GRG", "Soldier M4", "Soldier M249", "Soldier RPG" },
-- [DCSEx.enums.unitFamily.GROUND_MBT] = { "Challenger2", "Leclerc", "Leopard-2", "Leopard1A3", "M-1 Abrams", "Merkava_Mk4" },
-- [DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B" },
-- [DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Land_Rover_101_FC", "Land_Rover_109_S3", "M 818", "CHAP_M1083" },
-- [DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "AH-1W", "AH-64D", "OH-58D", "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" },
-- [DCSEx.enums.unitFamily.HELICOPTER_TRANSPORT] = { "CH-47D", "CH-53E", "SH-60B", "UH-60A" },
-- [DCSEx.enums.unitFamily.PLANE_ATTACK] = { "A-10C_2" },
-- [DCSEx.enums.unitFamily.PLANE_AWACS] = { "E-2C", "E-3A" },
-- [DCSEx.enums.unitFamily.PLANE_BOMBER] = { "B-1B Lancer", "B-52H" },
-- [DCSEx.enums.unitFamily.PLANE_FIGHTER] = { "F-16C_50", "FA-18C_hornet" },
-- [DCSEx.enums.unitFamily.PLANE_TANKER] = { "KC-135", "KC135MPRS" },
-- [DCSEx.enums.unitFamily.PLANE_TRANSPORT] = { "C-17A", "C-130" },
-- [DCSEx.enums.unitFamily.PLANE_UAV] = { "RQ-1A Predator" },
-- [DCSEx.enums.unitFamily.SHIP_CARGO] = { "Dry-cargo ship-1", "Dry-cargo ship-2", "ELNYA", "Ship_Tilde_Supply" },
-- [DCSEx.enums.unitFamily.SHIP_CARRIER] = { "CVN_71", "CVN_72", "CVN_73", "CVN_75", "hms_invincible", "LHA_Tarawa", "Stennis" },
-- [DCSEx.enums.unitFamily.SHIP_CRUISER] = { "TICONDEROG" },
-- [DCSEx.enums.unitFamily.SHIP_FRIGATE] = { "PERRY", "USS_Arleigh_Burke_IIa" },
-- [DCSEx.enums.unitFamily.SHIP_LIGHT] = { "speedboat" },
-- [DCSEx.enums.unitFamily.SHIP_MISSILE_BOAT] = { "CastleClass_01", "La_Combattante_II" },
-- [DCSEx.enums.unitFamily.SHIP_SUBMARINE] = { "santafe" },
-- -- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Bunker", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Sandbox", "Workshop A" },
-- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Workshop A" },
-- }
-- end
+3 -1
View File
@@ -21,7 +21,7 @@ do
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Ural-375 ZU-23", "ZSU_57_2", "ZSU-23-4 Shilka" }, [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Ural-375 ZU-23", "ZSU_57_2", "ZSU-23-4 Shilka" },
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = { "ZU-23 Emplacement Closed", "ZU-23 Emplacement" }, [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = { "ZU-23 Emplacement Closed", "ZU-23 Emplacement" },
[DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = { "SA-18 Igla manpad", "SA-18 Igla-S manpad" }, [DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = { "SA-18 Igla manpad", "SA-18 Igla-S manpad" },
-- [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*SA-2", "*SA-10" }, [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*SA-2", "*SA-10" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*SA-10" }, [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*SA-10" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*SA-3", "*SA-6", "*SA-11" }, [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*SA-3", "*SA-6", "*SA-11" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "2S6 Tunguska", "Osa 9A33 ln", "Tor 9A331", "CHAP_PantsirS1", "CHAP_TorM2" }, [DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "2S6 Tunguska", "Osa 9A33 ln", "Tor 9A331", "CHAP_PantsirS1", "CHAP_TorM2" },
@@ -45,6 +45,8 @@ do
[DCSEx.enums.unitFamily.PLANE_TRANSPORT] = { "An-26B", "An-30M", "IL-76MD" }, [DCSEx.enums.unitFamily.PLANE_TRANSPORT] = { "An-26B", "An-30M", "IL-76MD" },
[DCSEx.enums.unitFamily.PLANE_UAV] = { "WingLoong-I" }, [DCSEx.enums.unitFamily.PLANE_UAV] = { "WingLoong-I" },
[DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "Ka-50", "Mi-24V", "Mi-28N" },
[DCSEx.enums.unitFamily.SHIP_CARGO] = { "Dry-cargo ship-1", "Dry-cargo ship-2", "ELNYA", "Ship_Tilde_Supply" }, [DCSEx.enums.unitFamily.SHIP_CARGO] = { "Dry-cargo ship-1", "Dry-cargo ship-2", "ELNYA", "Ship_Tilde_Supply" },
[DCSEx.enums.unitFamily.SHIP_CARRIER] = { "CV_1143_5", "KUZNECOW" }, [DCSEx.enums.unitFamily.SHIP_CARRIER] = { "CV_1143_5", "KUZNECOW" },
[DCSEx.enums.unitFamily.SHIP_CRUISER] = { "MOSCOW", "PIOTR" }, [DCSEx.enums.unitFamily.SHIP_CRUISER] = { "MOSCOW", "PIOTR" },
+1 -39
View File
@@ -10,45 +10,7 @@ do
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.WORLD_WAR_2] = {} Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.WORLD_WAR_2] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.KOREA_WAR] = {} Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.KOREA_WAR] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.VIETNAM_WAR] = {} Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.VIETNAM_WAR] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.COLD_WAR] = {}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.COLD_WAR] = {
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" },
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_STATIC] = { "Gepard", "Vulcan" },
[DCSEx.enums.unitFamily.AIRDEFENSE_MANPADS] = { "Soldier stinger" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_LONG] = { "*Patriot" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_MEDIUM] = { "*HAWK", "*NASAMS" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT] = { "rapier_fsa", "Roland ADS" },
[DCSEx.enums.unitFamily.AIRDEFENSE_SAM_SHORT_IR] = { "M6 Linebacker", "M48 Chaparral", "M1097 Avenger" },
[DCSEx.enums.unitFamily.GROUND_APC] = { "AAV7", "Cobra", "LAV-25", "M-2 Bradley", "M-113", "M1045 HMMWV TOW", "M1126 Stryker ICV", "M1128 Stryker MGS", "Marder", "MCV-80", "MLRS FDDM", "TPZ", "CHAP_M1130", "CHAP_MATV" },
[DCSEx.enums.unitFamily.GROUND_ARTILLERY] = { "M-109", "MLRS", "CHAP_M142_ATACMS_M48", "CHAP_M142_GMLRS_M31" },
[DCSEx.enums.unitFamily.GROUND_INFANTRY] = { "Soldier M4 GRG", "Soldier M4", "Soldier M249", "Soldier RPG" },
[DCSEx.enums.unitFamily.GROUND_MBT] = { "Challenger2", "Leclerc", "Leopard-2", "Leopard1A3", "M-1 Abrams", "Merkava_Mk4" },
[DCSEx.enums.unitFamily.GROUND_SS_MISSILE] = { "Scud_B" },
[DCSEx.enums.unitFamily.GROUND_UNARMED] = { "Land_Rover_101_FC", "Land_Rover_109_S3", "M 818", "CHAP_M1083" },
[DCSEx.enums.unitFamily.HELICOPTER_ATTACK] = { "AH-1W", "AH-64D", "OH-58D", "SA342L", "SA342M", "SA342Minigun", "SA342Mistral" },
[DCSEx.enums.unitFamily.HELICOPTER_TRANSPORT] = { "CH-47D", "CH-53E", "SH-60B", "UH-60A" },
[DCSEx.enums.unitFamily.PLANE_ATTACK] = { "A-10C_2" },
[DCSEx.enums.unitFamily.PLANE_AWACS] = { "E-2C", "E-3A" },
[DCSEx.enums.unitFamily.PLANE_BOMBER] = { "B-1B Lancer", "B-52H" },
[DCSEx.enums.unitFamily.PLANE_FIGHTER] = { "F-16C_50", "FA-18C_hornet" },
[DCSEx.enums.unitFamily.PLANE_TANKER] = { "KC-135", "KC135MPRS" },
[DCSEx.enums.unitFamily.PLANE_TRANSPORT] = { "C-17A", "C-130" },
[DCSEx.enums.unitFamily.PLANE_UAV] = { "RQ-1A Predator" },
[DCSEx.enums.unitFamily.SHIP_CARGO] = { "Dry-cargo ship-1", "Dry-cargo ship-2", "ELNYA", "Ship_Tilde_Supply" },
[DCSEx.enums.unitFamily.SHIP_CARRIER] = { "CVN_71", "CVN_72", "CVN_73", "CVN_75", "hms_invincible", "LHA_Tarawa", "Stennis" },
[DCSEx.enums.unitFamily.SHIP_CRUISER] = { "TICONDEROG" },
[DCSEx.enums.unitFamily.SHIP_FRIGATE] = { "PERRY", "USS_Arleigh_Burke_IIa" },
[DCSEx.enums.unitFamily.SHIP_LIGHT] = { "speedboat" },
[DCSEx.enums.unitFamily.SHIP_MISSILE_BOAT] = { "CastleClass_01", "La_Combattante_II" },
[DCSEx.enums.unitFamily.SHIP_SUBMARINE] = { "santafe" },
-- [DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Bunker", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Sandbox", "Workshop A" },
[DCSEx.enums.unitFamily.STATIC_STRUCTURE] = { "af_hq", ".Command Center", "Building01_PBR", "Building02_PBR", "Building03_PBR", "Building04_PBR", "Building05_PBR", "Chemical tank A", "Comms tower M", "FARP Fuel Depot", "outpost", "Workshop A" },
}
Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.MODERN] = { Library.factions.tables["USA"].units[DCSEx.enums.timePeriod.MODERN] = {
[DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" }, [DCSEx.enums.unitFamily.AIRDEFENSE_AAA_MOBILE] = { "Gepard", "Vulcan" },
@@ -1,22 +0,0 @@
-- Library.tasks.helicopterDestroyInfantry = {
-- taskFamily = DCSEx.enums.taskFamily.HELICOPTER,
-- description =
-- {
-- briefing = {
-- "",
-- },
-- short = "Neutralize enemy infantry",
-- },
-- conditions = {
-- difficultyMinimum = 0,
-- eras = {},
-- },
-- completionEvent = DCSEx.enums.taskEvent.DESTROY,
-- flags = { },
-- minimumDistance = DCSEx.converter.nmToMeters(5.0),
-- safeRadius = 100,
-- surfaceType = nil,
-- targetCount = { 6, 8 },
-- targetFamilies = { DCSEx.enums.unitFamily.GROUND_INFANTRY },
-- waypointInaccuracy = DCSEx.converter.nmToMeters(1.5)
-- }
@@ -1,22 +0,0 @@
-- Library.tasks.helicopterPickUpInfantry = {
-- taskFamily = DCSEx.enums.taskFamily.HELICOPTER,
-- description =
-- {
-- briefing = {
-- "",
-- },
-- short = "Land and pick up friendly infantry",
-- },
-- conditions = {
-- difficultyMinimum = 0,
-- eras = {},
-- },
-- completionEvent = DCSEx.enums.taskEvent.LAND,
-- flags = { DCSEx.enums.taskFlag.FRIENDLY_TARGET },
-- minimumDistance = DCSEx.converter.nmToMeters(5.0),
-- safeRadius = 100,
-- surfaceType = nil,
-- targetCount = { 6, 8 },
-- targetFamilies = { DCSEx.enums.unitFamily.GROUND_INFANTRY },
-- waypointInaccuracy = DCSEx.converter.nmToMeters(1.5)
-- }
@@ -1,22 +1,22 @@
Library.tasks.ocaStrategicAircraftStrike = { Library.tasks.ocaBomberStrike = {
taskFamily = DCSEx.enums.taskFamily.OCA, taskFamily = DCSEx.enums.taskFamily.OCA,
description = description =
{ {
briefing = { briefing = {
"" ""
}, },
short = "Destroy enemy aircraft on the ramp", short = "Destroy enemy bomber on the ramp",
}, },
conditions = { conditions = {
difficultyMinimum = 0, difficultyMinimum = 0,
eras = {}, eras = {},
}, },
completionEvent = DCSEx.enums.taskEvent.DAMAGE, completionEvent = DCSEx.enums.taskEvent.DESTROY,
flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET }, flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET },
minimumDistance = 0.0, minimumDistance = 0.0,
safeRadius = 0, safeRadius = 0,
surfaceType = land.SurfaceType.LAND, surfaceType = land.SurfaceType.LAND,
targetCount = { 1, 1 }, targetCount = { 1, 1 },
targetFamilies = { DCSEx.enums.unitFamily.PLANE_BOMBER, DCSEx.enums.unitFamily.PLANE_TRANSPORT }, targetFamilies = { DCSEx.enums.unitFamily.PLANE_BOMBER },
waypointInaccuracy = 0.0 waypointInaccuracy = 0.0
} }
+25 -25
View File
@@ -1,25 +1,25 @@
-- Library.tasks.ocaFighterStrike = { Library.tasks.ocaFighterStrike = {
-- taskFamily = DCSEx.enums.taskFamily.OCA, taskFamily = DCSEx.enums.taskFamily.OCA,
-- description = description =
-- { {
-- briefing = { briefing = {
-- "Destroying enemy fighters on the ramp will prevent immediate sortie generation and blunt their ability to provide CAS against our advancing forces.", "Destroying enemy fighters on the ramp will prevent immediate sortie generation and blunt their ability to provide CAS against our advancing forces.",
-- "A ramp strike against enemy aircraft will pre-empt an imminent scramble warning we have intel for, preventing a coordinated mass launch.", "A ramp strike against enemy aircraft will pre-empt an imminent scramble warning we have intel for, preventing a coordinated mass launch.",
-- "Removing enemy fighters on the ramp will degrade enemy air superiority over the battlespace and protects our medevac and resupply corridors.", "Removing enemy fighters on the ramp will degrade enemy air superiority over the battlespace and protects our medevac and resupply corridors.",
-- "Priority is to eliminate immediate airborne threats on the ramp to safeguard coalition force freedom of maneuver.", "Priority is to eliminate immediate airborne threats on the ramp to safeguard coalition force freedom of maneuver.",
-- }, },
-- short = "Destroy enemy fighter on the ramp", short = "Destroy enemy fighter on the ramp",
-- }, },
-- conditions = { conditions = {
-- difficultyMinimum = 0, difficultyMinimum = 0,
-- eras = {}, eras = {},
-- }, },
-- completionEvent = DCSEx.enums.taskEvent.DAMAGE, completionEvent = DCSEx.enums.taskEvent.DESTROY,
-- flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET }, flags = { DCSEx.enums.taskFlag.ALLOW_JTAC, DCSEx.enums.taskFlag.PARKED_AIRCRAFT_TARGET },
-- minimumDistance = 0.0, minimumDistance = 0.0,
-- safeRadius = 0, safeRadius = 0,
-- surfaceType = land.SurfaceType.LAND, surfaceType = land.SurfaceType.LAND,
-- targetCount = { 1, 1 }, targetCount = { 1, 1 },
-- targetFamilies = { DCSEx.enums.unitFamily.PLANE_FIGHTER }, targetFamilies = { DCSEx.enums.unitFamily.PLANE_FIGHTER },
-- waypointInaccuracy = 0.0 waypointInaccuracy = 0.0
-- } }
+1 -1
View File
@@ -8,7 +8,7 @@ IF %ERRORLEVEL% NEQ 0 goto ERROR-NO-PHP
@REM CREATE SCENARIOS @REM CREATE SCENARIOS
@REM ------------------------------------------- @REM -------------------------------------------
set buildConfig=p set buildConfig=p
echo THE UNIVERSAL MISSION BUILDER SCRIPT: echo THE ULTIMATE MISSION BUILDER SCRIPT:
echo - Build [P]ersianGulf debug theater only (default) echo - Build [P]ersianGulf debug theater only (default)
echo - Build all [D]ebug theaters echo - Build all [D]ebug theaters
echo - Build all [R]elease theaters echo - Build all [R]elease theaters
+11 -24
View File
@@ -1,6 +1,6 @@
# The Universal Mission for DCS World # The Universal Mission for DCS World
**Current version: open beta 0.3.251019** (see the "version history" section at the end of this file for a list of the latest changes) **Current version: open beta 0.3.250914** (see the "version history" section at the end of this file for a list of the latest changes)
**This is a BETA version, there may be bugs and there WILL be unbalanced stuff.** **This is a BETA version, there may be bugs and there WILL be unbalanced stuff.**
@@ -42,6 +42,7 @@ The Universal Mission for DCS World is a fully dynamic single-player/PvE mission
- Download the latest release from this GitHub page. - Download the latest release from this GitHub page.
- Copy the provided autoexec.cfg file to your **[Saved Games]\DCS\Config directory** - Copy the provided autoexec.cfg file to your **[Saved Games]\DCS\Config directory**
- Please note: as of DCS 2.9.18.12899, it seems the autoexec.cfg file [is no longer needed](https://www.digitalcombatsimulator.com/en/news/changelog/release/2.9.18.12899/) but I advise you to copy it anyway, ED might change its mind again.
- Copy the .miz files for your theater(s) of choice to your **[Saved Games]\DCS\Missions directory** - Copy the .miz files for your theater(s) of choice to your **[Saved Games]\DCS\Missions directory**
- _**(Optional but strongly recommended)**_ Unsanitize the Lua IO module. You don't have to do this, but the persistent career system won't work if you don't. To do it, open the file **[DCS World installation directory]\Scripts\MissionScripting.lua** with a text editor and comment or remove the line "sanitizeModule('io')". Make sure you restart DCS World once you've modified the file. - _**(Optional but strongly recommended)**_ Unsanitize the Lua IO module. You don't have to do this, but the persistent career system won't work if you don't. To do it, open the file **[DCS World installation directory]\Scripts\MissionScripting.lua** with a text editor and comment or remove the line "sanitizeModule('io')". Make sure you restart DCS World once you've modified the file.
- Please note: should you want to backup, delete or transfer it, career progress is saved in **[DCS World installation directory]\TheUniversalMission.sav** - Please note: should you want to backup, delete or transfer it, career progress is saved in **[DCS World installation directory]\TheUniversalMission.sav**
@@ -74,17 +75,17 @@ Please also note that PvP is not supported at the moment and that the mission wi
- Additional content - Additional content
- [ ] More objectives types - [ ] More objectives types
- [ ] Close air support - [x] Helicopter hunt
- [ ] Helicopter-specific tasking (land and pick up units, suppress infantry...) - [ ] Offensive counter-air
- [ ] Improved OCA missions: bomb enemy airbases
- Balance improvements - Balance improvements
- [ ] Night missions should award more XP
- [ ] Tweaked XP requirements for medals/promotions - [ ] Tweaked XP requirements for medals/promotions
- Bug fixes - Bug fixes
- [ ] AWACS datalinked contacts not showing on SA pages
- [X] AWACS can now detect enemy helicopters
- Extras - Extras
- [ ] GitHub page - [ ] GitHub page
- Improvements - Improvements
- Misc - [ ] Better weather reports (cloud cover, rain, etc)
- New features - New features
- [ ] Administrative settings menu - [ ] Administrative settings menu
- [ ] Friendly air defenses - [ ] Friendly air defenses
@@ -96,15 +97,17 @@ Please also note that PvP is not supported at the moment and that the mission wi
- Additional/improved radio messages - Additional/improved radio messages
- More "flavor" radio messages ("fence in" when player approaches the AO, etc) so the world will feel more alive - More "flavor" radio messages ("fence in" when player approaches the AO, etc) so the world will feel more alive
- Better balancing of the player career awards and promotions
- Better use of context for "ambient" radio messages (should only warn of a SAM launch if an AI pilot is there to witness it, etc) - Better use of context for "ambient" radio messages (should only warn of a SAM launch if an AI pilot is there to witness it, etc)
- Improved score multiplier taking into account various aspects of mission difficulty (weather, nighttime ops...)
- Laser designation of targets by JTAC - Laser designation of targets by JTAC
- New objectives: CAP - New objectives: helicopter (drop/pickup units...), CAP, CAS, OCA (airbase attack)
- Support for all missing DCS World theaters - Support for all missing DCS World theaters
- Support for more factions and five different time periods (World War 2, Korea war, Vietnam war, late Cold war, Modern) - Support for more factions and five different time periods (World War 2, Korea war, Vietnam war, late Cold war, Modern)
### Medium priority ### Medium priority
- Combined arms support - Combined Arms support
- Modded units support (other than player-controlled aircraft, those are already supported: just add them to the mission) - Modded units support (other than player-controlled aircraft, those are already supported: just add them to the mission)
- Spawning of tankers for long-range missions - Spawning of tankers for long-range missions
- (maybe) Text (not voiceover) localization, if there's enough popular demand - (maybe) Text (not voiceover) localization, if there's enough popular demand
@@ -154,22 +157,6 @@ The core script is quite simple and small, I probably won't need too much help w
## Version history ## Version history
- **0.3.251019** (10/19/2025)
- Additional content
- New mission types
- "Helicopter hunt" missions: locate and intercept enemy attack and transport helicopters
- Offensive counter-air: destroy enemy aircraft on the ramp before they take off **(Requires a target location with at least one enemy land airbase, or mission type will automatically be changed to ground attack)**
- Balance improvements
- Missions with higher cloud cover and wind speed now award more XP
- Bug fixes
- AWACS datalinked now showing on SA pages
- AWACS can now detect enemy helicopters
- Added missing net.allow_unsafe_api value in autoexec.cfg that prevented some advanced scripts from working
- "On landing" actions (medal/promotions check, AI wingmen removal, etc) no longer triggered when player lands away from an airfield (e.g. in a Harrier on a helicopter)
- Improvements
- ATC weather reports now include informations about cloud cover and rain
- Misc
- AWACS unit now spawned when mission is loaded
- **0.3.250914** (09/14/2025) - **0.3.250914** (09/14/2025)
- **MAJOR CHANGE:** - **MAJOR CHANGE:**
- Use of "Client" slots instead of "Player" slots even in single-player missions, allowing the player to respawn on death/ejection instead of having to start the whole mission again - Use of "Client" slots instead of "Player" slots even in single-player missions, allowing the player to respawn on death/ejection instead of having to start the whole mission again
+4 -6
View File
@@ -29,10 +29,9 @@ DCSEx.enums.lineType = {
-- Event to check to see if a task/objective is complete -- Event to check to see if a task/objective is complete
------------------------------------- -------------------------------------
DCSEx.enums.taskEvent = { DCSEx.enums.taskEvent = {
DAMAGE = 1, DESTROY = 1,
DESTROY = 2, DESTROY_SCENERY = 2,
DESTROY_SCENERY = 3, LAND = 3,
LAND = 4,
} }
------------------------------------- -------------------------------------
@@ -43,7 +42,7 @@ DCSEx.enums.taskFamily = {
-- CAP = XXX, -- CAP = XXX,
-- CAS = XXX, -- CAS = XXX,
GROUND_ATTACK = 2, GROUND_ATTACK = 2,
-- HELICOPTER = 3, -- HELICOPTER = XXX,
HELO_HUNT = 3, HELO_HUNT = 3,
INTERCEPTION = 4, INTERCEPTION = 4,
OCA = 5, OCA = 5,
@@ -62,7 +61,6 @@ DCSEx.enums.taskFlag = {
ON_ROADS = 5, ON_ROADS = 5,
PARKED_AIRCRAFT_TARGET = 6, PARKED_AIRCRAFT_TARGET = 6,
SCENERY_TARGET = 7, SCENERY_TARGET = 7,
FRIENDLY_TARGET = 8
} }
------------------------------------- -------------------------------------
+9 -22
View File
@@ -2,7 +2,7 @@
-- DCSEX.ZONES - FUNCTIONS RELATED TO MAP TRIGGER ZONES -- DCSEX.ZONES - FUNCTIONS RELATED TO MAP TRIGGER ZONES
-- ==================================================================================== -- ====================================================================================
-- DCSEx.zones.drawOnMap(zoneTable, lineColor, fillColor, lineType, drawName, readOnly) -- DCSEx.zones.drawOnMap(zoneTable, lineColor, fillColor, lineType, drawName, readOnly)
-- DCSEx.zones.getAirbases(zone, coalID, allowShips) -- DCSEx.zones.getAirbases(zone, coalition)
-- DCSEx.zones.getAll() -- DCSEx.zones.getAll()
-- DCSEx.zones.getByName(name) -- DCSEx.zones.getByName(name)
-- DCSEx.zones.getCenter(zoneTable) -- DCSEx.zones.getCenter(zoneTable)
@@ -77,33 +77,20 @@ end
-- Returns all airbases in the zone -- Returns all airbases in the zone
------------------------------------- -------------------------------------
-- @param zoneTable Table of the zone in which to search -- @param zoneTable Table of the zone in which to search
-- @param coalID Coalition (from the coalition.side enum) the airbase must belong to. Default is nil, which means "all coalitions" -- @param coalition Coalition (from the coalition.side enum) the airbase must belong to. Default is nil, which means "all coalitions"
-- @param allowShips Should ships be allowed?
-- @return Table of airbases -- @return Table of airbases
------------------------------------- -------------------------------------
function DCSEx.zones.getAirbases(zoneTable, coalID, allowShips) function DCSEx.zones.getAirbases(zoneTable, coalition)
coalID = coalID or nil coalition = coalition or nil
allowShips = allowShips or false
local coalitionSides = { coalition.side.RED, coalition.side.BLUE } local coalitions = { coalition.side.RED, coalition.side.BLUE }
if coalID then coalitionSides = { coalID } end if coalition then coalitions = { coalition } end
local validAirbases = {} local validAirbases = {}
for _,side in ipairs(coalitionSides) do for _,side in ipairs(coalitions) do
for _,ab in ipairs(coalition.getAirbases(side)) do for _,ab in ipairs(coalition.getAirbases(side)) do
local abDesc = ab:getDesc() if DCSEx.zones.isPointInside(zoneTable, ab:getPoint()) then
local isValid = true table.insert(validAirbases, ab)
if ab:getDesc().category == Airbase.Category.HELIPAD then
isValid = false
elseif ab:getDesc().category == Airbase.Category.SHIP and not allowShips then
isValid = false
end
if isValid then
if DCSEx.zones.isPointInside(zoneTable, ab:getPoint()) then
table.insert(validAirbases, ab)
end
end end
end end
end end
-2
View File
@@ -167,8 +167,6 @@ function TUM.initialize()
timer.scheduleFunction(TUM.onClockTick, nil, timer.getTime() + math.random(10, 15)) timer.scheduleFunction(TUM.onClockTick, nil, timer.getTime() + math.random(10, 15))
end end
end end
TUM.supportAWACS.create()
end end
if TUM.administrativeSettings.getValue(TUM.administrativeSettings.INITIALIZE_AUTOMATICALLY) then if TUM.administrativeSettings.getValue(TUM.administrativeSettings.INITIALIZE_AUTOMATICALLY) then
+4 -7
View File
@@ -132,17 +132,14 @@ do
commonWeatherInfo = commonWeatherInfo.." (day, sunset at "..DCSEx.string.getTimeString(Library.environment.getDayTime(nil, true))..")\n" commonWeatherInfo = commonWeatherInfo.." (day, sunset at "..DCSEx.string.getTimeString(Library.environment.getDayTime(nil, true))..")\n"
end end
commonWeatherInfo = commonWeatherInfo.."- Cloud cover: "..TUM.weather.getWeatherName(nil, true).."\n"
commonWeatherInfo = commonWeatherInfo.."- Wind: "..TUM.weather.getWindName()..", with avg. speed of "..tostring(math.floor(Library.environment.getWindAverage())).."m/s\n"
commonWeatherInfo = commonWeatherInfo.."- Average ground-level temperature is "..getTemperatureCelsiusAndFarenheit(env.mission.weather.season.temperature).."\n"
local players = coalition.getPlayers(TUM.settings.getPlayerCoalition()) local players = coalition.getPlayers(TUM.settings.getPlayerCoalition())
for _,p in ipairs(players) do for _,p in ipairs(players) do
local lTemperature, _ = atmosphere.getTemperatureAndPressure(p:getPoint()) local lTemperature, _ = atmosphere.getTemperatureAndPressure(p:getPoint())
local localWeatherInfo = "" local localWeatherInfo = "- Average windspeed is "..tostring(math.floor(Library.environment.getWindAverage())).."m/s\n"
localWeatherInfo = localWeatherInfo.."- Wind speed at your location is "..tostring(math.floor(DCSEx.math.getLength3D(atmosphere.getWind(p:getPoint())))).."m/s\n" localWeatherInfo = localWeatherInfo.."- Windspeed at your location is "..DCSEx.math.getLength3D(atmosphere.getWind(p:getPoint())).."m/s\n"
localWeatherInfo = localWeatherInfo.."- Temperature at your location is "..getTemperatureCelsiusAndFarenheit(lTemperature, true).."\n" localWeatherInfo = localWeatherInfo.."- Average ground-level temperature is "..getTemperatureCelsiusAndFarenheit(env.mission.weather.season.temperature).."\n"
localWeatherInfo = localWeatherInfo.."- Temperature at your location is "..getTemperatureCelsiusAndFarenheit(lTemperature, true)
TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcWeatherUpdate", { commonWeatherInfo..localWeatherInfo }, "Control", delayRadioAnswer) TUM.radio.playForUnit(DCSEx.dcs.getObjectIDAsNumber(p), "atcWeatherUpdate", { commonWeatherInfo..localWeatherInfo }, "Control", delayRadioAnswer)
end end
@@ -225,9 +225,11 @@ do
if not event.initiator then return end -- No event initiator if not event.initiator then return end -- No event initiator
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Initiator isn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end -- Not a friendly
if not event.place then return end -- Not landed at an airbase (e.g. helicopter landing on the ground)
local baseName = event.place:getName():upper() local baseName = "AIRBASE"
if event.place then
baseName = event.place:getName():upper()
end
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) or not event.initiator:getPlayerName() then if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) or not event.initiator:getPlayerName() then
doAmbientChatter("atcSafeLanding", {event.initiator:getCallsign(), baseName}, baseName.." ATC", 1) doAmbientChatter("atcSafeLanding", {event.initiator:getCallsign(), baseName}, baseName.." ATC", 1)
+1 -14
View File
@@ -32,7 +32,7 @@ do
end end
-- Called when an unit is destroyed -- Called when a unit is destroyed
local function onEventDead(event) local function onEventDead(event)
if not event.initiator then return end -- Nothing was hit if not event.initiator then return end -- Nothing was hit
@@ -53,17 +53,6 @@ do
) )
end end
-- Called when an unit takes damage
local function onEventHit(event)
if not event.initiator then return end -- Nothing was hit
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Target wasn't an unit
if event.initiator:getCoalition() ~= TUM.settings.getEnemyCoalition() then return end -- Unit is not an enemy
if event.initiator:getDesc().category ~= Unit.Category.AIRPLANE and event.initiator:getDesc().category ~= Unit.Category.HELICOPTER then return end -- Wasn't an aircraft
if event.initiator:inAir() then return end -- Unit is currently in air
trigger.action.explosion(event.initiator:getPoint(), 100) -- Detonate the parked aircraft, to make it easier to kill
end
function TUM.ambientWorld.removeAll() function TUM.ambientWorld.removeAll()
for _,id in ipairs(groupIDs) do for _,id in ipairs(groupIDs) do
DCSEx.world.destroyGroupByID(id) DCSEx.world.destroyGroupByID(id)
@@ -81,8 +70,6 @@ do
if event.id == world.event.S_EVENT_DEAD then if event.id == world.event.S_EVENT_DEAD then
onEventDead(event) onEventDead(event)
elseif event.id == world.event.S_EVENT_HIT then
onEventHit(event)
end end
end end
end end
+1 -4
View File
@@ -68,10 +68,7 @@ do
local runwayTouchEvent = { id = world.event.S_EVENT_RUNWAY_TOUCH, initiator = playerUnit } local runwayTouchEvent = { id = world.event.S_EVENT_RUNWAY_TOUCH, initiator = playerUnit }
TUM.onEvent(runwayTouchEvent) TUM.onEvent(runwayTouchEvent)
local friendlyAirbases = coalition.getAirbases(TUM.settings.getPlayerCoalition()) local landingEvent = { id = world.event.S_EVENT_LAND, initiator = playerUnit }
if not friendlyAirbases or #friendlyAirbases == 0 then return end
local landingEvent = { id = world.event.S_EVENT_LAND, place = friendlyAirbases[1], initiator = playerUnit }
timer.scheduleFunction(TUM.onEvent, landingEvent, timer.getTime() + 1) timer.scheduleFunction(TUM.onEvent, landingEvent, timer.getTime() + 1)
end end
+1 -10
View File
@@ -69,16 +69,7 @@ do
end end
end end
-- If an OCA mission has been selected and the selected target zone doesn't contain any enemy airfield, default to ground attack trigger.action.outText("Generating mission and loading assets, this can take some time...", 5)
if TUM.settings.getValue(TUM.settings.id.TASKING) == DCSEx.enums.taskFamily.OCA then
local zone = DCSEx.zones.getByName(TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true))
if #DCSEx.zones.getAirbases(zone, TUM.settings.getEnemyCoalition()) == 0 then
trigger.action.outText("OCA tasking selected in a zone without any enemy airfields, defaulting to GROUND ATTACK tasking.", 3)
TUM.settings.setValue(TUM.settings.id.TASKING, DCSEx.enums.taskFamily.GROUND_ATTACK, true)
end
end
trigger.action.outText("Generating mission and loading assets, this can take some time...", 3)
-- Add a little delay for the "Generating mission..." message be printed out. Once generation begins, the main DCS thread will be to busy to output anything. -- Add a little delay for the "Generating mission..." message be printed out. Once generation begins, the main DCS thread will be to busy to output anything.
timer.scheduleFunction(TUM.mission.beginMission, false, timer.getTime() + 1) timer.scheduleFunction(TUM.mission.beginMission, false, timer.getTime() + 1)
+1 -2
View File
@@ -62,7 +62,6 @@ do
closeMission(true) closeMission(true)
TUM.intermission.removeMissionZonesMarkers() TUM.intermission.removeMissionZonesMarkers()
TUM.objectivesMaker.clear()
for _=1,TUM.settings.getValue(TUM.settings.id.TARGET_COUNT) do for _=1,TUM.settings.getValue(TUM.settings.id.TARGET_COUNT) do
TUM.objectives.add() TUM.objectives.add()
end end
@@ -73,7 +72,7 @@ do
return return
end end
-- TUM.supportAWACS.create() -- Create the AWACS aircraft if it wasn't airborne already TUM.supportAWACS.create() -- Create the AWACS aircraft if it wasn't airborne already
TUM.enemyAirDefense.create() -- Must be called once objectives have been created TUM.enemyAirDefense.create() -- Must be called once objectives have been created
TUM.airForce.create() -- Must be called once objectives have been created TUM.airForce.create() -- Must be called once objectives have been created
TUM.missionMenu.create() -- Must be called once objectives have been created TUM.missionMenu.create() -- Must be called once objectives have been created
+2 -3
View File
@@ -15,6 +15,7 @@ do
-- @param event A DCS World event, possibly a S_EVENT_LAND event -- @param event A DCS World event, possibly a S_EVENT_LAND event
------------------------------------- -------------------------------------
local function removeAIAircraftOnLandEvent(event) local function removeAIAircraftOnLandEvent(event)
if event.id ~= world.event.S_EVENT_LAND then return end
if not event.initiator then return end if not event.initiator then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Not an unit if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end -- Not an unit
if event.initiator:getPlayerName() then return end -- Don't remove player aircraft, that would cause horrendous bugs if event.initiator:getPlayerName() then return end -- Don't remove player aircraft, that would cause horrendous bugs
@@ -72,8 +73,6 @@ do
-- @param event The DCS World event -- @param event The DCS World event
------------------------------------- -------------------------------------
function TUM.mizCleaner.onEvent(event) function TUM.mizCleaner.onEvent(event)
if event.id == world.event.S_EVENT_LAND and event.place then removeAIAircraftOnLandEvent(event)
removeAIAircraftOnLandEvent(event)
end
end end
end end
+2 -27
View File
@@ -168,36 +168,11 @@ do
end end
local function onObjectiveEvent(index, event) local function onObjectiveEvent(index, event)
if not event.initiator then return end
if index < 1 or index > #objectives then return end -- Out of bounds if index < 1 or index > #objectives then return end -- Out of bounds
if objectives[index].completed then return end -- Objective already completed if objectives[index].completed then return end -- Objective already completed
local completionEvent = Library.tasks[objectives[index].taskID].completionEvent if event.id ~= world.event.S_EVENT_DEAD and event.id ~= world.event.S_EVENT_UNIT_LOST then return end
if not event.initiator then return end
if completionEvent == DCSEx.enums.taskEvent.DAMAGE then
if event.id ~= world.event.S_EVENT_DEAD and event.id ~= world.event.S_EVENT_HIT and event.id ~= world.event.S_EVENT_UNIT_LOST then return end
elseif completionEvent == DCSEx.enums.taskEvent.DESTROY then
if event.id ~= world.event.S_EVENT_DEAD and event.id ~= world.event.S_EVENT_UNIT_LOST then return end
elseif completionEvent == DCSEx.enums.taskEvent.LAND then
if event.id ~= world.event.S_EVENT_LAND then return end
if Object.getCategory(event.initiator) ~= Object.Category.UNIT then return end
if event.initiator:getDesc().category ~= Unit.Category.HELICOPTER then return end
if event.initiator:getCoalition() ~= TUM.settings.getPlayerCoalition() then return end
if DCSEx.math.getDistance2D(DCSEx.math.vec3ToVec2(event.initiator:getPoint()), objectives[index].point2) > 500 then return end -- Too far from objective
-- Remove target group if it exists (to simulate it was picked up/captured)
if objectives[index].groupID then
local targetGroup = DCSEx.world.getGroupByID(objectives[index].groupID)
if targetGroup then
targetGroup:destroy()
end
end
timer.scheduleFunction(markObjectiveAsComplete, index, timer.getTime() + 3)
updateObjectiveText(index)
return
end
if objectives[index].isAirbaseTarget then if objectives[index].isAirbaseTarget then
if Object.getCategory(event.initiator) == Object.Category.BASE then if Object.getCategory(event.initiator) == Object.Category.BASE then
@@ -6,8 +6,6 @@
TUM.objectivesMaker = {} TUM.objectivesMaker = {}
do do
local usedParkingSpots = {}
local function pickRandomTask() local function pickRandomTask()
local taskFamily = TUM.settings.getValue(TUM.settings.id.TASKING) local taskFamily = TUM.settings.getValue(TUM.settings.id.TASKING)
@@ -43,10 +41,6 @@ do
return possiblePoints[1] return possiblePoints[1]
end end
function TUM.objectivesMaker.clear()
usedParkingSpots = {}
end
function TUM.objectivesMaker.create() function TUM.objectivesMaker.create()
local zone = DCSEx.zones.getByName(TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true)) local zone = DCSEx.zones.getByName(TUM.settings.getValue(TUM.settings.id.TARGET_LOCATION, true))
@@ -94,17 +88,13 @@ do
local parkings = pickedAirbase:getParking() local parkings = pickedAirbase:getParking()
local validParkings = {} local validParkings = {}
for _,p in pairs(parkings) do for _,p in pairs(parkings) do
local parkingUniqueID = pickedAirbase:getID() * 10000 + p.Term_Index if p.Term_Type == 104 then table.insert(validParkings, p) end
if p.Term_Type == 104 and not DCSEx.table.contains(usedParkingSpots, parkingUniqueID) then
table.insert(validParkings, p)
end
end end
if #validParkings == 0 then if #validParkings == 0 then
TUM.log("Failed to find a valid airbase parking to spawn a target.", TUM.logger.logLevel.WARNING) TUM.log("Failed to find a valid airbase parking to spawn a target.", TUM.logger.logLevel.WARNING)
return nil return nil
end end
local pickedParking = DCSEx.table.getRandom(validParkings) local pickedParking = DCSEx.table.getRandom(validParkings)
table.insert(usedParkingSpots, pickedAirbase:getID() * 10000 + pickedParking.Term_Index) -- Mark parking spot as used so it won't be taken by another objective
parkingInfo = { airbaseID = pickedAirbase:getID(), parkingID = pickedParking.Term_Index } parkingInfo = { airbaseID = pickedAirbase:getID(), parkingID = pickedParking.Term_Index }
spawnPoint3 = pickedParking.vTerminalPos spawnPoint3 = pickedParking.vTerminalPos
spawnPoint2 = DCSEx.math.vec3ToVec2(spawnPoint3) spawnPoint2 = DCSEx.math.vec3ToVec2(spawnPoint3)
@@ -170,23 +160,10 @@ do
-- Parked aircraft only -- Parked aircraft only
if parkingInfo then if parkingInfo then
groupOptions.airbaseID = parkingInfo.airbaseID groupOptions.airbaseID = parkingInfo.airbaseID
groupOptions.invisible = true -- Not ideal because wingmen can't be tasked with attacking targets, but only way I've found to prevent friendly CAP from attacking parked aircraft
groupOptions.parkingID = parkingInfo.parkingID groupOptions.parkingID = parkingInfo.parkingID
end end
-- Target group belongs to the enemy coalition, unless DCSEx.enums.taskFlag.FRIENDLY_TARGET is set local units = Library.factions.getUnits(TUM.settings.getEnemyFaction(), objectiveDB.targetFamilies, math.random(objectiveDB.targetCount[1], objectiveDB.targetCount[2]))
local groupCoalition = TUM.settings.getEnemyCoalition()
local groupFaction = TUM.settings.getEnemyFaction()
if DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.FRIENDLY_TARGET) then
groupCoalition = TUM.settings.getPlayerCoalition()
groupFaction = TUM.settings.getPlayerFaction()
-- Friendly target groups are immortal and invisible, so AI won't kill them before the player got a chance to interact with them
groupOptions.immortal = true
groupOptions.invisible = true
end
local units = Library.factions.getUnits(groupFaction, objectiveDB.targetFamilies, math.random(objectiveDB.targetCount[1], objectiveDB.targetCount[2]))
local groupInfo = nil local groupInfo = nil
if objectiveDB.targetFamilies[1] == DCSEx.enums.unitFamily.STATIC_STRUCTURE then if objectiveDB.targetFamilies[1] == DCSEx.enums.unitFamily.STATIC_STRUCTURE then
@@ -195,7 +172,7 @@ do
groupInfo.unitsID = { DCSEx.unitGroupMaker.createStatic(TUM.settings.getEnemyCoalition(), objective.point2, units[1], "") } groupInfo.unitsID = { DCSEx.unitGroupMaker.createStatic(TUM.settings.getEnemyCoalition(), objective.point2, units[1], "") }
end end
else else
groupInfo = DCSEx.unitGroupMaker.create(groupCoalition, DCSEx.dcs.getUnitCategoryFromFamily(objectiveDB.targetFamilies[1]), objective.point2, units, groupOptions) groupInfo = DCSEx.unitGroupMaker.create(TUM.settings.getEnemyCoalition(), DCSEx.dcs.getUnitCategoryFromFamily(objectiveDB.targetFamilies[1]), objective.point2, units, groupOptions)
end end
if not groupInfo then if not groupInfo then
+2 -14
View File
@@ -55,12 +55,7 @@ do
if not objectDesc or not objectDesc.attributes then return 10 end -- No description, assume a default value of 10 points if not objectDesc or not objectDesc.attributes then return 10 end -- No description, assume a default value of 10 points
local groundMultiplier = 1 local groundMultiplier = 1
if not killedObject:inAir() then if not killedObject:inAir() then groundMultiplier = 0.5 end -- Aircraft killed on the ground are worth less points
-- Aircraft killed on the ground are worth less points, except AWACS, bombers and transports
if not objectDesc.attributes["AWACS"] and not objectDesc.attributes["Transports"] and not objectDesc.attributes["Strategic bombers"] then
groundMultiplier = 0.5
end
end
-- Misc -- Misc
if objectDesc.attributes["Missiles"] then return 10 end if objectDesc.attributes["Missiles"] then return 10 end
@@ -300,18 +295,11 @@ do
if not DCSEx.io.canReadAndWrite() then return 1.0 end -- IO disabled, career and scoring disabled if not DCSEx.io.canReadAndWrite() then return 1.0 end -- IO disabled, career and scoring disabled
if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 1.0 end -- No scoring in multiplayer if TUM.settings.getValue(TUM.settings.id.MULTIPLAYER) then return 1.0 end -- No scoring in multiplayer
-- Base XP multiplier is 1.0 (100%)
local scoreMultiplier = 1.0 local scoreMultiplier = 1.0
-- Add XP multipliers for game settings
for _,v in pairs(TUM.settings.id) do for _,v in pairs(TUM.settings.id) do
scoreMultiplier = scoreMultiplier + (TUM.playerScore.getScoreMultiplier(v) or 0.0) scoreMultiplier = scoreMultiplier + (TUM.playerScore.getScoreMultiplier(v) or 0.0)
end end
-- Add XP multipliers for weather
scoreMultiplier = scoreMultiplier + TUM.weather.getWeatherXPModifier()
scoreMultiplier = scoreMultiplier + TUM.weather.getWindXPModifier()
return math.max(0.0, scoreMultiplier) return math.max(0.0, scoreMultiplier)
end end
@@ -347,7 +335,7 @@ do
return return
end end
if event.id == world.event.S_EVENT_LAND and event.place then if event.id == world.event.S_EVENT_LAND then
onLandEvent(event) onLandEvent(event)
return return
end end
+1 -6
View File
@@ -52,8 +52,7 @@ do
[TUM.settings.id.PLAYER_COALITION] = { "Red", "Blue" }, -- Must match values in the coalition.side enum [TUM.settings.id.PLAYER_COALITION] = { "Red", "Blue" }, -- Must match values in the coalition.side enum
[TUM.settings.id.TARGET_COUNT] = { "1", "2", "3", "4" }, [TUM.settings.id.TARGET_COUNT] = { "1", "2", "3", "4" },
[TUM.settings.id.TARGET_LOCATION] = { }, [TUM.settings.id.TARGET_LOCATION] = { },
-- [TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Helicopter-specific tasks", "Helo hunt", "Interception", "Offensive counter-air", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum [TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Helicopter hunt", "Interception", "OCA", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum
[TUM.settings.id.TASKING] = { "Antiship strike", "Ground attack", "Helo hunt", "Interception", "Offensive counter-air", "SEAD", "Strike" }, -- Must match values in the DCSEx.enums.taskFamily enum
[TUM.settings.id.TIME_PERIOD] = { "World War 2", "Korea War", "Vietnam War", "Late Cold War", "Modern" }, -- Must match values in the DCSEx.enums.timePeriod enum [TUM.settings.id.TIME_PERIOD] = { "World War 2", "Korea War", "Vietnam War", "Late Cold War", "Modern" }, -- Must match values in the DCSEx.enums.timePeriod enum
[TUM.settings.id.WINGMEN] = { "None", "1", "2", "3" } [TUM.settings.id.WINGMEN] = { "None", "1", "2", "3" }
} }
@@ -194,10 +193,6 @@ do
end end
end end
-- Weather
summary = summary.."\n\nWEATHER: "..TUM.weather.getWeatherName().." (+"..tostring(math.ceil(TUM.weather.getWeatherXPModifier() * 100)).."% xp)"
summary = summary.."\nWIND: "..DCSEx.string.firstToUpper(TUM.weather.getWindName()).." (+"..tostring(math.ceil(TUM.weather.getWindXPModifier() * 100)).."% xp)"
if showScoreMultiplier then if showScoreMultiplier then
summary = summary.."\n\nTotal XP modifier: "..tostring(math.ceil(TUM.playerScore.getTotalScoreMultiplier() * 100)).."%" summary = summary.."\n\nTotal XP modifier: "..tostring(math.ceil(TUM.playerScore.getTotalScoreMultiplier() * 100)).."%"
end end
+75 -2
View File
@@ -1,6 +1,13 @@
-- ==================================================================================== -- ====================================================================================
-- TUM.SUPPORTJTAC - HANDLES FRIENDLY JTAC SMOKE MARKERS AND LASING -- TUM.SUPPORTJTAC - HANDLES FRIENDLY JTAC SMOKE MARKERS AND LASING
-- ==================================================================================== -- ====================================================================================
-- (local) destroyAllLasers()
-- (local) doCommandLaser(index)
-- (local) doCommandLaserStop(index)
-- (local) doCommandSmoke(index)
-- (local) spawnSmoke(args)
-- (local) updateLaserPositions()
-- TUM.supportJTAC.setupJTACOnObjective(index, menuRoot)
-- ==================================================================================== -- ====================================================================================
TUM.supportJTAC = {} TUM.supportJTAC = {}
@@ -28,14 +35,57 @@ do
"Whiplash", "Whiplash",
} }
local LASER_CODE = 1688
local LASER_DESIGNATION_PENALTY = -15
local SMOKE_DURATION = 300 -- in seconds local SMOKE_DURATION = 300 -- in seconds
local SMOKE_MARKER_PENALTY = -25 local SMOKE_MARKER_PENALTY = -25
local jtacName = {} local jtacName = {}
local lastSmoke = {} local lastSmoke = {}
local laserSpots = {}
local laserTargets = {}
local function spawnSmoke(args) local function destroyAllLasers()
trigger.action.smoke(args.point3, args.smokeColor) if #laserSpots == 0 and #laserTargets == 0 then return end
for _,l in pairs(laserSpots) do
if l then l:destroy() end
end
laserSpots = {}
laserTargets = {}
end
local function doCommandLaser(index)
local obj = TUM.objectives.getObjective(index)
if not obj then return end
if laserSpots[index] then
laserSpots[index]:destroy()
laserSpots[index] = nil
end
local sourceObject = nil
local targetPoint = {x = 0, y = 0, z = 0}
laserSpots[index] = Spot.createLaser(sourceObject, {x = 0, y = 1, z = 0}, targetPoint, LASER_CODE)
end
local function doCommandLaserStop(index)
local obj = TUM.objectives.getObjective(index)
if not obj then return end
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "playerJTACLaserStop", { jtacName[index], obj.name }, TUM.mission.getPlayerCallsign(), false)
-- Not lasing this objective at the moment
if not laserSpots[index] then
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacLaserStopNoLaser", { jtacName[index], obj.name }, jtacName[index], true)
return
end
laserSpots[index]:destroy()
laserSpots[index] = nil
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacLaserStop", { jtacName[index], obj.name }, jtacName[index], true)
end end
local function doCommandSmoke(index) local function doCommandSmoke(index)
@@ -79,6 +129,24 @@ do
TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeNoTarget", { jtacName[index] }, jtacName[index], true) TUM.radio.playForCoalition(TUM.settings.getPlayerCoalition(), "jtacSmokeNoTarget", { jtacName[index] }, jtacName[index], true)
end end
local function spawnSmoke(args)
trigger.action.smoke(args.point3, args.smokeColor)
end
local function updateLaserPositions()
-- Not in a mission? Destroy all lasers
if TUM.mission.getStatus() == TUM.mission.status.NONE then
destroyAllLasers()
return timer.getTime() + 0.5
end
for i=1,TUM.objectives.getCount() do
-- TODO
end
return timer.getTime() + 0.5
end
function TUM.supportJTAC.setupJTACOnObjective(index, menuRoot) function TUM.supportJTAC.setupJTACOnObjective(index, menuRoot)
local obj = TUM.objectives.getObjective(index) local obj = TUM.objectives.getObjective(index)
if not obj then return end if not obj then return end
@@ -89,5 +157,10 @@ do
if not DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.ALLOW_JTAC) then return end -- No JTAC for this objective if not DCSEx.table.contains(objectiveDB.flags, DCSEx.enums.taskFlag.ALLOW_JTAC) then return end -- No JTAC for this objective
missionCommands.addCommand("Smoke marker on target ("..tostring(SMOKE_MARKER_PENALTY).."xp)", menuRoot, doCommandSmoke, index) missionCommands.addCommand("Smoke marker on target ("..tostring(SMOKE_MARKER_PENALTY).."xp)", menuRoot, doCommandSmoke, index)
missionCommands.addCommand("Designate target with laser (code "..tostring(LASER_CODE)..", "..tostring(LASER_DESIGNATION_PENALTY).."xp)", menuRoot, doCommandLaser, index)
missionCommands.addCommand("Stop designating target with laser", menuRoot, doCommandLaserStop, index)
end end
-- Schedule the laser updating function to be called every 0.5 seconds
timer.scheduleFunction(updateLaserPositions, {}, timer.getTime() + 0.5)
end end
-322
View File
@@ -1,322 +0,0 @@
-- ====================================================================================
-- TUM.WEATHER - HANDLES THE MISSION'S WEATHER SETTINGS
-- ====================================================================================
-- ====================================================================================
TUM.weather = {}
do
local cloudPresets = {
-- "Light Scattered 1",
Preset1 = {
readableName = "Few scattered clouds (METAR: FEW/SCT 7/8)",
readableNameShort = "Light scattered",
xpBonus = 0,
},
-- "Light Scattered 2",
Preset2 = {
readableName = "Two layers few and scattered (METAR: FEW/SCT 8/10 SCT 23/24)",
readableNameShort = "Light scattered",
xpBonus = 0,
},
-- "High Scattered 1",
Preset3 = {
readableName = "Two layers scattered (METAR: SCT 8/9 FEW 21)",
readableNameShort = "High Scattered",
xpBonus = 0,
},
-- "High Scattered 2",
Preset4 = {
readableName = "Two layers scattered (METAR: SCT 8/10 FEW/SCT 24/26)",
readableNameShort = "High Scattered",
xpBonus = 0,
},
-- "Scattered 1",
Preset5 = {
readableName = "Three layers high altitude scattered (METAR: SCT 14/17 FEW 27/29 BKN 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 2",
Preset6 = {
readableName = "One layer scattered/broken (METAR: SCT/BKN 8/10 FEW 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 3",
Preset7 = {
readableName = "Two layers scattered/broken (METAR: BKN 7.5/12 SCT/BKN 21/23 SCT 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "High Scattered 3",
Preset8 = {
readableName = "Two layers scattered/broken high altitude (METAR: SCT/BKN 18/20 FEW 36/38 FEW 40)",
readableNameShort = "High Scattered",
xpBonus = 0,
},
-- "Scattered 4",
Preset9 = {
readableName = "Two layers broken/scattered (METAR: BKN 7.5/10 SCT 20/22 FEW41)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 5",
Preset10 = {
readableName = "Two layers scattered large thick clouds (METAR: SCT/BKN 18/20 FEW36/38 FEW 40)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 6",
Preset11 = {
readableName = "Two layers scattered large clouds high ceiling (METAR: BKN 18/20 BKN 32/33 FEW 41)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Scattered 7",
Preset12 = {
readableName = "Two layers scattered large clouds high ceiling (METAR: BKN 12/14 SCT 22/23 FEW 41)",
readableNameShort = "Scattered",
xpBonus = 0,
},
-- "Broken 1",
Preset13 = {
readableName = "Two layers broken clouds (METAR: BKN 12/14 BKN 26/28 FEW 41)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 2",
Preset14 = {
readableName = "Broken thick low layer with few high layers\nMETAR: BKN LYR 7/16 FEW 41)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 3",
Preset15 = {
readableName = "Two layers broken large clouds (METAR: SCT/BKN 14/18 BKN 24/27 FEW 40)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 4",
Preset16 = {
readableName = "Two layers broken large clouds (METAR: BKN 14/18 BKN 28/30 FEW 40)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 5",
Preset17 = {
readableName = "Three layers broken/overcast (METAR: BKN/OVC LYR 7/13 20/22 32/34)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 6",
Preset18 = {
readableName = "Three layers broken/overcast (METAR: BKN/OVC LYR 13/15 25/29 38/41)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 7",
Preset19 = {
readableName = "Three layers overcast at low level (METAR: OVC 9/16 BKN/OVC LYR 23/24 31/33)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Broken 8",
Preset20 = {
readableName = "Three layers overcast low level (METAR: BKN/OVC 13/18 BKN 28/30 SCT FEW 38)",
readableNameShort = "Broken",
xpBonus = 0.05,
},
-- "Overcast 1",
Preset21 = {
readableName = "Overcast low level (METAR: BKN/OVC LYR 7/8 17/19)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 2",
Preset22 = {
readableName = "Overcast low level (METAR: BKN LYR 7/10 17/20)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 3",
Preset23 = {
readableName = "Three layers broken low level scattered high (METAR: BKN LYR 11/14 18/25 SCT 32/35)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 4",
Preset24 = {
readableName = "Three layers overcast (METAR: BKN/OVC 3/7 17/22 BKN 34)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 5",
Preset25 = {
readableName = "Three layers overcast (METAR: OVC LYR 12/14 22/25 40/42)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 6",
Preset26 = {
readableName = "Three layers overcast (METAR: OVC 9/15 BKN 23/25 SCT 32)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast 7",
Preset27 = {
readableName = "Three layer overcast (METAR: OVC 8/15 SCT/BKN 25/26 34/36)",
readableNameShort = "Overcast",
xpBonus = 0.10,
},
-- "Overcast And Rain 1",
RainyPreset1 = {
readableName = "Overcast with rain (METAR: VIS 3-5KM RA OVC 3/15 28/30 FEW 40)",
readableNameShort = "Overcast and rain",
xpBonus = 0.25,
},
-- "Overcast And Rain 2",
RainyPreset2 = {
readableName = "Overcast with rain (METAR: VIS 1-5KM RA BKN/OVC 3/11 SCT 18/29 FEW 40)",
readableNameShort = "Overcast and rain",
xpBonus = 0.25,
},
-- "Overcast And Rain 3",
RainyPreset3 = {
readableName = "Overcast with rain (METAR: VIS 3-5KM RA OVC LYR 6/18 19/21 SCT 34)",
readableNameShort = "Overcast and rain",
xpBonus = 0.25,
},
-- "Light Rain 1",
RainyPreset4 = {
readableName = "Two layers scattered large thick clouds (METAR: SCT/BKN 18/20 FEW36/38 FEW 40)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
-- "Light Rain 2",
RainyPreset5 = {
readableName = "Three layers broken/overcast (METAR: BKN/OVC LYR 7/13 20/22 32/34)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
-- "Light Rain 3",
RainyPreset6 = {
readableName = "Three layers overcast at low level (METAR: OVC 9/16 BKN/OVC LYR 23/24 31/33)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
-- "Light Rain 4",
NEWRAINPRESET4 = {
readableName = "Two layers overcast at low level (METAR: OVC 9/16 BKN/OVC LYR 23/24 31/33)",
readableNameShort = "Light rain",
xpBonus = 0.15,
},
}
local function getWindBeaufortScale(speedInMS)
local speedInKMH = DCSEx.converter.mpsToKmph(speedInMS or Library.environment.getWindAverage())
if speedInKMH < 1 then return 0
elseif speedInKMH <= 5 then return 1
elseif speedInKMH <= 11 then return 2
elseif speedInKMH <= 19 then return 3
elseif speedInKMH <= 28 then return 4
elseif speedInKMH <= 38 then return 5
elseif speedInKMH <= 49 then return 6
elseif speedInKMH <= 61 then return 7
elseif speedInKMH <= 74 then return 8
elseif speedInKMH <= 88 then return 9
elseif speedInKMH <= 102 then return 10
elseif speedInKMH <= 117 then return 11
else return 12
end
end
function TUM.weather.getWeatherName(presetID, longForm)
presetID = presetID or env.mission.weather.clouds.preset
longForm = longForm or false
if cloudPresets[presetID] == nil then return "Unknown" end
if longForm then return cloudPresets[presetID].readableName end
return cloudPresets[presetID].readableNameShort
end
function TUM.weather.getWeatherXPModifier(presetID)
presetID = presetID or env.mission.weather.clouds.preset
if cloudPresets[presetID] == nil then return 0 end
return cloudPresets[presetID].xpBonus
end
function TUM.weather.getWindName(speedInMS)
local windBeaufort = getWindBeaufortScale(speedInMS)
if windBeaufort == 0 then return "calm"
elseif windBeaufort == 1 then return "light air"
elseif windBeaufort == 2 then return "light breeze"
elseif windBeaufort == 3 then return "gentle breeze"
elseif windBeaufort == 4 then return "moderate breeze"
elseif windBeaufort == 5 then return "fresh breeze"
elseif windBeaufort == 6 then return "strong breeze"
elseif windBeaufort == 7 then return "moderate gale"
elseif windBeaufort == 8 then return "fresh gale"
elseif windBeaufort == 9 then return "strong gale"
elseif windBeaufort == 10 then return "storm"
elseif windBeaufort == 11 then return "violent storm"
elseif windBeaufort == 12 then return "hurricane"
end
end
function TUM.weather.getWindXPModifier(speedInMS)
local windBeaufort = getWindBeaufortScale(speedInMS)
if windBeaufort == 0 then return 0.00
elseif windBeaufort == 1 then return 0.02
elseif windBeaufort == 2 then return 0.04
elseif windBeaufort == 3 then return 0.08
elseif windBeaufort == 4 then return 0.10
elseif windBeaufort == 5 then return 0.12
elseif windBeaufort == 6 then return 0.15
elseif windBeaufort == 7 then return 0.18
elseif windBeaufort == 8 then return 0.21
elseif windBeaufort == 9 then return 0.24
elseif windBeaufort == 10 then return 0.27
elseif windBeaufort == 11 then return 0.30
elseif windBeaufort == 12 then return 0.33
end
end
end
+1 -1
View File
@@ -177,7 +177,7 @@ do
if not event.initiator:getPlayerName() then return end if not event.initiator:getPlayerName() then return end
if TUM.mission.getStatus() == TUM.mission.status.NONE then return end -- Mission not in progress, no wingman needed if TUM.mission.getStatus() == TUM.mission.status.NONE then return end -- Mission not in progress, no wingman needed
TUM.wingmen.create() TUM.wingmen.create()
elseif event.id == world.event.S_EVENT_LAND and event.place then -- Remove wingmen on player landing elseif event.id == world.event.S_EVENT_LAND then -- Remove wingmen on player landing
if not event.initiator:getPlayerName() then return end if not event.initiator:getPlayerName() then return end
TUM.wingmen.removeAll() TUM.wingmen.removeAll()
elseif event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then -- Remove wingmen when player takes control of a new unit elseif event.id == world.event.S_EVENT_PLAYER_ENTER_UNIT then -- Remove wingmen when player takes control of a new unit
+2 -4
View File
@@ -5,7 +5,7 @@
User's manual User's manual
</p> </p>
<p class="heavy"> <p class="heavy">
The Universal Mission v0.3.251019<br /> The Universal Mission v0.3.250914<br />
Created and maintained by Ambroise Garel (<a href='mailto:akaagarmail@gmail.com'>akaagarmail@gmail.com</a>) Created and maintained by Ambroise Garel (<a href='mailto:akaagarmail@gmail.com'>akaagarmail@gmail.com</a>)
</p> </p>
<p class="heavy"> <p class="heavy">
@@ -73,6 +73,7 @@ I hope you'll like it.
- Download the latest release from this GitHub page. - Download the latest release from this GitHub page.
- Copy the provided autoexec.cfg file to your **[Saved Games]\DCS\Config directory** - Copy the provided autoexec.cfg file to your **[Saved Games]\DCS\Config directory**
- Please note: as of DCS 2.9.18.12899, it seems the autoexec.cfg file [is no longer needed](https://www.digitalcombatsimulator.com/en/news/changelog/release/2.9.18.12899/) but I advise you to copy it anyway, ED might change its mind again.
- Copy the .miz files for your theater(s) of choice to your **[Saved Games]\DCS\Missions directory** - Copy the .miz files for your theater(s) of choice to your **[Saved Games]\DCS\Missions directory**
- _**(Optional but strongly recommended)**_ Unsanitize the Lua IO module. You don't have to do this, but the persistent career system won't work if you don't. To do it, open the file **[DCS World installation directory]\Scripts\MissionScripting.lua** with a text editor and comment or remove the line "sanitizeModule('io')". Make sure you restart DCS World once you've modified the file. - _**(Optional but strongly recommended)**_ Unsanitize the Lua IO module. You don't have to do this, but the persistent career system won't work if you don't. To do it, open the file **[DCS World installation directory]\Scripts\MissionScripting.lua** with a text editor and comment or remove the line "sanitizeModule('io')". Make sure you restart DCS World once you've modified the file.
- Please note: should you want to backup, delete or transfer it, career progress is saved in **[DCS World installation directory]\TheUniversalMission.sav** - Please note: should you want to backup, delete or transfer it, career progress is saved in **[DCS World installation directory]\TheUniversalMission.sav**
@@ -112,10 +113,7 @@ The exact content of the menu will depend on the current phase of the mission.
- **Mission type**: What will your mission be? - **Mission type**: What will your mission be?
- **Antiship strike**: Sink enemy warships and cargo ships. - **Antiship strike**: Sink enemy warships and cargo ships.
- **Ground attack**: Interdiction missions against armor, artillery and convoys. - **Ground attack**: Interdiction missions against armor, artillery and convoys.
- **Helicopter-specific tasks**: Tasks specifically designed for helicopters (lift/pick up friendly units, suppress infantry...)
- **Helicopter hunt**: Shoot down enemy transport and attack helicopters.
- **Interception**: Shoot down strategic airplanes (bombers, transports...) and enemy attack planes on interdiction missions. - **Interception**: Shoot down strategic airplanes (bombers, transports...) and enemy attack planes on interdiction missions.
- **Offensive counter-air**: Bomb enemy airbases and destroy parked aircraft. **(Requires a target area with at least one enemy land airbase, or mission type will automatically be changed to ground attack)**
- **SEAD**: Destroy enemy SAM sites. - **SEAD**: Destroy enemy SAM sites.
- **Strike**: Destroy enemy structures and civilian buildings occupied by enemy forces. - **Strike**: Destroy enemy structures and civilian buildings occupied by enemy forces.
- **Target location**: Where on the map will the targets be spawned? Approximate distance to possible regions is displayed in the menu. - **Target location**: Where on the map will the targets be spawned? Approximate distance to possible regions is displayed in the menu.