diff --git a/.github/workflows/build-includes.yml b/.github/workflows/build-includes.yml
index 15065ba9e..dd1f90e6a 100644
--- a/.github/workflows/build-includes.yml
+++ b/.github/workflows/build-includes.yml
@@ -47,6 +47,7 @@ jobs:
- name: Update apt-get (needed for act docker image)
run: |
+ sudo rm /etc/apt/sources.list.d/microsoft-prod.list
sudo apt-get -qq update
- name: Install tree
diff --git a/Moose Development/Moose/Core/Beacon.lua b/Moose Development/Moose/Core/Beacon.lua
index 7cc9434d6..92c72eb2f 100644
--- a/Moose Development/Moose/Core/Beacon.lua
+++ b/Moose Development/Moose/Core/Beacon.lua
@@ -38,11 +38,13 @@
-- @type BEACON
-- @field #string ClassName Name of the class "BEACON".
-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{Wrapper.Controllable#CONTROLLABLE} that will receive radio capabilities.
+-- @field #number UniqueName Counter to make the unique naming work.
-- @extends Core.Base#BASE
BEACON = {
ClassName = "BEACON",
Positionable = nil,
name = nil,
+ UniqueName = 0,
}
--- Beacon types supported by DCS.
@@ -384,7 +386,9 @@ end
function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration)
self:F({FileName, Frequency, Modulation, Power, BeaconDuration})
local IsValid = false
-
+
+ Modulation = Modulation or radio.modulation.AM
+
-- Check the filename
if type(FileName) == "string" then
if FileName:find(".ogg") or FileName:find(".wav") then
@@ -395,7 +399,7 @@ function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDurati
end
end
if not IsValid then
- self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName})
+ self:E({"File name invalid. Maybe something wrong with the extension? ", FileName})
end
-- Check the Frequency
@@ -421,7 +425,9 @@ function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDurati
if IsValid then
self:T2({"Activating Beacon on ", Frequency, Modulation})
-- Note that this is looped. I have to give this transmission a unique name, I use the class ID
- trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID))
+ BEACON.UniqueName = BEACON.UniqueName + 1
+ self.BeaconName = "MooseBeacon"..tostring(BEACON.UniqueName)
+ trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, self.BeaconName)
if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
SCHEDULER:New( nil,
@@ -429,7 +435,8 @@ function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDurati
self:StopRadioBeacon()
end, {}, BeaconDuration)
end
- end
+ end
+ return self
end
--- Stops the Radio Beacon
@@ -438,7 +445,7 @@ end
function BEACON:StopRadioBeacon()
self:F()
-- The unique name of the transmission is the class ID
- trigger.action.stopRadioTransmission(tostring(self.ID))
+ trigger.action.stopRadioTransmission(self.BeaconName)
return self
end
diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua
index bcc348814..dae6195d2 100644
--- a/Moose Development/Moose/Core/ClientMenu.lua
+++ b/Moose Development/Moose/Core/ClientMenu.lua
@@ -20,7 +20,7 @@
--
-- @module Core.ClientMenu
-- @image Core_Menu.JPG
--- last change: Apr 2024
+-- last change: May 2024
-- TODO
----------------------------------------------------------------------------------------------------------------
@@ -691,7 +691,7 @@ function CLIENTMENUMANAGER:Propagate(Client)
local client = _client -- Wrapper.Client#CLIENT
if client and client:IsAlive() then
local playerunit = client:GetName()
- local playergroup = client:GetGroup()
+ --local playergroup = client:GetGroup()
local playername = client:GetPlayerName() or "none"
if not knownunits[playerunit] then
knownunits[playerunit] = true
diff --git a/Moose Development/Moose/Core/Database.lua b/Moose Development/Moose/Core/Database.lua
index 8f031fdd0..db54e8dc2 100644
--- a/Moose Development/Moose/Core/Database.lua
+++ b/Moose Development/Moose/Core/Database.lua
@@ -1147,10 +1147,13 @@ end
-- @param #string GroupName Group name.
-- @return #table Group template table.
function DATABASE:GetGroupTemplate( GroupName )
- local GroupTemplate = self.Templates.Groups[GroupName].Template
- GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID
- GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID
- GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID
+ local GroupTemplate=nil
+ if self.Templates.Groups[GroupName] then
+ GroupTemplate = self.Templates.Groups[GroupName].Template
+ GroupTemplate.SpawnCoalitionID = self.Templates.Groups[GroupName].CoalitionID
+ GroupTemplate.SpawnCategoryID = self.Templates.Groups[GroupName].CategoryID
+ GroupTemplate.SpawnCountryID = self.Templates.Groups[GroupName].CountryID
+ end
return GroupTemplate
end
diff --git a/Moose Development/Moose/Core/Event.lua b/Moose Development/Moose/Core/Event.lua
index 5908c032b..78b69202a 100644
--- a/Moose Development/Moose/Core/Event.lua
+++ b/Moose Development/Moose/Core/Event.lua
@@ -1301,7 +1301,7 @@ function EVENT:onEvent( Event )
-- STATIC
---
Event.TgtDCSUnit = Event.target
- if Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object
+ if Event.target.isExist and Event.target:isExist() and Event.id ~= 33 then -- leave out ejected seat object, check that isExist exists (Kiowa Hellfire issue, Special K)
Event.TgtDCSUnitName = Event.TgtDCSUnit:getName()
-- Workaround for borked target info on cruise missiles
if Event.TgtDCSUnitName and Event.TgtDCSUnitName ~= "" then
diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua
index fc9a75394..44c15b08d 100644
--- a/Moose Development/Moose/Core/Menu.lua
+++ b/Moose Development/Moose/Core/Menu.lua
@@ -1,9 +1,9 @@
--- **Core** - Manage hierarchical menu structures and commands for players within a mission.
---
+--
-- ===
---
--- ### Features:
---
+--
+-- ## Features:
+--
-- * Setup mission sub menus.
-- * Setup mission command menus.
-- * Setup coalition sub menus.
@@ -21,35 +21,39 @@
-- * Update the parameters and the receiving methods, without updating the menu within DCS!
-- * Provide a great performance boost in menu management.
-- * Provide a great tool to manage menus in your code.
---
--- DCS Menus can be managed using the MENU classes.
--- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scenarios where you need to
+--
+-- DCS Menus can be managed using the MENU classes.
+-- The advantage of using MENU classes is that it hides the complexity of dealing with menu management in more advanced scenarios where you need to
-- set menus and later remove them, and later set them again. You'll find while using use normal DCS scripting functions, that setting and removing
--- menus is not a easy feat if you have complex menu hierarchies defined.
+-- menus is not a easy feat if you have complex menu hierarchies defined.
-- Using the MOOSE menu classes, the removal and refreshing of menus are nicely being handled within these classes, and becomes much more easy.
--- On top, MOOSE implements **variable parameter** passing for command menus.
---
+-- On top, MOOSE implements **variable parameter** passing for command menus.
+--
-- There are basically two different MENU class types that you need to use:
---
+--
-- ### To manage **main menus**, the classes begin with **MENU_**:
---
+--
-- * @{Core.Menu#MENU_MISSION}: Manages main menus for whole mission file.
-- * @{Core.Menu#MENU_COALITION}: Manages main menus for whole coalition.
-- * @{Core.Menu#MENU_GROUP}: Manages main menus for GROUPs.
---
+--
-- ### To manage **command menus**, which are menus that allow the player to issue **functions**, the classes begin with **MENU_COMMAND_**:
---
+--
-- * @{Core.Menu#MENU_MISSION_COMMAND}: Manages command menus for whole mission file.
-- * @{Core.Menu#MENU_COALITION_COMMAND}: Manages command menus for whole coalition.
-- * @{Core.Menu#MENU_GROUP_COMMAND}: Manages command menus for GROUPs.
---
+--
-- ===
----
+--
+-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_Demos/tree/master/Core/Menu)
+--
+-- ===
+--
-- ### Author: **FlightControl**
--- ### Contributions:
---
+-- ### Contributions:
+--
-- ===
---
+--
-- @module Core.Menu
-- @image Core_Menu.JPG
@@ -65,18 +69,18 @@ MENU_INDEX.Group = {}
function MENU_INDEX:ParentPath( ParentMenu, MenuText )
local Path = ParentMenu and "@" .. table.concat( ParentMenu.MenuPath or {}, "@" ) or ""
- if ParentMenu then
+ if ParentMenu then
if ParentMenu:IsInstanceOf( "MENU_GROUP" ) or ParentMenu:IsInstanceOf( "MENU_GROUP_COMMAND" ) then
local GroupName = ParentMenu.Group:GetName()
if not self.Group[GroupName].Menus[Path] then
- BASE:E( { Path = Path, GroupName = GroupName } )
+ BASE:E( { Path = Path, GroupName = GroupName } )
error( "Parent path not found in menu index for group menu" )
return nil
end
elseif ParentMenu:IsInstanceOf( "MENU_COALITION" ) or ParentMenu:IsInstanceOf( "MENU_COALITION_COMMAND" ) then
local Coalition = ParentMenu.Coalition
if not self.Coalition[Coalition].Menus[Path] then
- BASE:E( { Path = Path, Coalition = Coalition } )
+ BASE:E( { Path = Path, Coalition = Coalition } )
error( "Parent path not found in menu index for coalition menu" )
return nil
end
@@ -88,7 +92,7 @@ function MENU_INDEX:ParentPath( ParentMenu, MenuText )
end
end
end
-
+
Path = Path .. "@" .. MenuText
return Path
end
@@ -149,24 +153,25 @@ function MENU_INDEX:ClearGroupMenu( Group, Path )
end
function MENU_INDEX:Refresh( Group )
for MenuID, Menu in pairs( self.MenuMission.Menus ) do
- Menu:Refresh()
- end
+ Menu:Refresh()
+ end
for MenuID, Menu in pairs( self.Coalition[coalition.side.BLUE].Menus ) do
- Menu:Refresh()
- end
+ Menu:Refresh()
+ end
for MenuID, Menu in pairs( self.Coalition[coalition.side.RED].Menus ) do
- Menu:Refresh()
- end
+ Menu:Refresh()
+ end
local GroupName = Group:GetName()
for MenuID, Menu in pairs( self.Group[GroupName].Menus ) do
- Menu:Refresh()
- end
-
+ Menu:Refresh()
+ end
+
return self
end
do -- MENU_BASE
- --- @type MENU_BASE
+ ---
+ -- @type MENU_BASE
-- @extends Core.Base#BASE
--- Defines the main MENU class where other MENU classes are derived from.
-- This is an abstract class, so don't use it.
@@ -177,19 +182,19 @@ do -- MENU_BASE
MenuText = "",
MenuParentPath = nil,
}
-
+
--- Constructor
-- @param #MENU_BASE
-- @return #MENU_BASE
function MENU_BASE:New( MenuText, ParentMenu )
-
+
local MenuParentPath = {}
if ParentMenu ~= nil then
MenuParentPath = ParentMenu.MenuPath
end
local self = BASE:Inherit( self, BASE:New() )
-
- self.MenuPath = nil
+
+ self.MenuPath = nil
self.MenuText = MenuText
self.ParentMenu = ParentMenu
self.MenuParentPath = MenuParentPath
@@ -198,14 +203,15 @@ do -- MENU_BASE
self.MenuCount = 0
self.MenuStamp = timer.getTime()
self.MenuRemoveParent = false
-
+
if self.ParentMenu then
self.ParentMenu.Menus = self.ParentMenu.Menus or {}
self.ParentMenu.Menus[MenuText] = self
end
-
+
return self
end
+
function MENU_BASE:SetParentMenu( MenuText, Menu )
if self.ParentMenu then
self.ParentMenu.Menus = self.ParentMenu.Menus or {}
@@ -231,7 +237,7 @@ do -- MENU_BASE
self.MenuRemoveParent = RemoveParent
return self
end
-
+
--- Gets a @{Menu} from a parent @{Menu}
-- @param #MENU_BASE self
-- @param #string MenuText The text of the child menu.
@@ -239,7 +245,7 @@ do -- MENU_BASE
function MENU_BASE:GetMenu( MenuText )
return self.Menus[MenuText]
end
-
+
--- Sets a menu stamp for later prevention of menu removal.
-- @param #MENU_BASE self
-- @param MenuStamp
@@ -248,16 +254,16 @@ do -- MENU_BASE
self.MenuStamp = MenuStamp
return self
end
-
-
+
+
--- Gets a menu stamp for later prevention of menu removal.
-- @param #MENU_BASE self
-- @return MenuStamp
function MENU_BASE:GetStamp()
return timer.getTime()
end
-
-
+
+
--- Sets a time stamp for later prevention of menu removal.
-- @param #MENU_BASE self
-- @param MenuStamp
@@ -266,7 +272,7 @@ do -- MENU_BASE
self.MenuStamp = MenuStamp
return self
end
-
+
--- Sets a tag for later selection of menu refresh.
-- @param #MENU_BASE self
-- @param #string MenuTag A Tag or Key that will filter only menu items set with this key.
@@ -275,16 +281,18 @@ do -- MENU_BASE
self.MenuTag = MenuTag
return self
end
-
+
end
-do -- MENU_COMMAND_BASE
- --- @type MENU_COMMAND_BASE
+do
+ ---
+ -- MENU_COMMAND_BASE
+ -- @type MENU_COMMAND_BASE
-- @field #function MenuCallHandler
-- @extends Core.Menu#MENU_BASE
-
- --- Defines the main MENU class where other MENU COMMAND_
+
+ --- Defines the main MENU class where other MENU COMMAND_
-- classes are derived from, in order to set commands.
- --
+ --
-- @field #MENU_COMMAND_BASE
MENU_COMMAND_BASE = {
ClassName = "MENU_COMMAND_BASE",
@@ -292,12 +300,12 @@ do -- MENU_COMMAND_BASE
CommandMenuArgument = nil,
MenuCallHandler = nil,
}
-
+
--- Constructor
-- @param #MENU_COMMAND_BASE
-- @return #MENU_COMMAND_BASE
function MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, CommandMenuArguments )
-
+
local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) ) -- #MENU_COMMAND_BASE
-- When a menu function goes into error, DCS displays an obscure menu message.
-- This error handler catches the menu error and displays the full call stack.
@@ -308,20 +316,20 @@ do -- MENU_COMMAND_BASE
end
return errmsg
end
-
+
self:SetCommandMenuFunction( CommandMenuFunction )
self:SetCommandMenuArguments( CommandMenuArguments )
self.MenuCallHandler = function()
- local function MenuFunction()
+ local function MenuFunction()
return self.CommandMenuFunction( unpack( self.CommandMenuArguments ) )
end
local Status, Result = xpcall( MenuFunction, ErrorHandler )
end
-
+
return self
end
-
- --- This sets the new command function of a menu,
+
+ --- This sets the new command function of a menu,
-- so that if a menu is regenerated, or if command function changes,
-- that the function set for the menu is loosely coupled with the menu itself!!!
-- If the function changes, no new menu needs to be generated if the menu text is the same!!!
@@ -331,7 +339,7 @@ do -- MENU_COMMAND_BASE
self.CommandMenuFunction = CommandMenuFunction
return self
end
- --- This sets the new command arguments of a menu,
+ --- This sets the new command arguments of a menu,
-- so that if a menu is regenerated, or if command arguments change,
-- that the arguments set for the menu are loosely coupled with the menu itself!!!
-- If the arguments change, no new menu needs to be generated if the menu text is the same!!!
@@ -343,41 +351,43 @@ do -- MENU_COMMAND_BASE
end
end
-do -- MENU_MISSION
- --- @type MENU_MISSION
+do
+ ---
+ -- MENU_MISSION
+ -- @type MENU_MISSION
-- @extends Core.Menu#MENU_BASE
- --- Manages the main menus for a complete mission.
- --
+ --- Manages the main menus for a complete mission.
+ --
-- You can add menus with the @{#MENU_MISSION.New} method, which constructs a MENU_MISSION object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION.Remove}.
-- @field #MENU_MISSION
MENU_MISSION = {
ClassName = "MENU_MISSION",
}
-
+
--- MENU_MISSION constructor. Creates a new MENU_MISSION object and creates the menu for a complete mission file.
-- @param #MENU_MISSION self
-- @param #string MenuText The text for the menu.
-- @param #table ParentMenu The parent menu. This parameter can be ignored if you want the menu to be located at the parent menu of DCS world (under F10 other).
-- @return #MENU_MISSION
function MENU_MISSION:New( MenuText, ParentMenu )
-
+
MENU_INDEX:PrepareMission()
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
- local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
+ local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
if MissionMenu then
return MissionMenu
else
local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) )
MENU_INDEX:SetMissionMenu( Path, self )
-
+
self.MenuPath = missionCommands.addSubMenu( self.MenuText, self.MenuParentPath )
self:SetParentMenu( self.MenuText, self )
return self
end
-
+
end
-
+
--- Refreshes a radio item for a mission
-- @param #MENU_MISSION self
-- @return #MENU_MISSION
@@ -388,28 +398,28 @@ do -- MENU_MISSION
end
return self
end
-
+
--- Removes the sub menus recursively of this MENU_MISSION. Note that the main menu is kept!
-- @param #MENU_MISSION self
-- @return #MENU_MISSION
function MENU_MISSION:RemoveSubMenus()
-
+
for MenuID, Menu in pairs( self.Menus or {} ) do
Menu:Remove()
end
-
+
self.Menus = nil
-
+
end
-
+
--- Removes the main menu and the sub menus recursively of this MENU_MISSION.
-- @param #MENU_MISSION self
-- @return #nil
function MENU_MISSION:Remove( MenuStamp, MenuTag )
-
+
MENU_INDEX:PrepareMission()
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
+ local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
if MissionMenu == self then
self:RemoveSubMenus()
if not MenuStamp or self.MenuStamp ~= MenuStamp then
@@ -426,26 +436,26 @@ do -- MENU_MISSION
else
BASE:E( { "Cannot Remove MENU_MISSION", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText } )
end
-
+
return self
end
end
do -- MENU_MISSION_COMMAND
-
+
--- @type MENU_MISSION_COMMAND
-- @extends Core.Menu#MENU_COMMAND_BASE
-
- --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution.
- --
+
+ --- Manages the command menus for a complete mission, which allow players to execute functions during mission execution.
+ --
-- You can add menus with the @{#MENU_MISSION_COMMAND.New} method, which constructs a MENU_MISSION_COMMAND object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_MISSION_COMMAND.Remove}.
- --
+ --
-- @field #MENU_MISSION_COMMAND
MENU_MISSION_COMMAND = {
ClassName = "MENU_MISSION_COMMAND",
}
-
+
--- MENU_MISSION constructor. Creates a new radio command item for a complete mission file, which can invoke a function with parameters.
-- @param #MENU_MISSION_COMMAND self
-- @param #string MenuText The text for the menu.
@@ -454,10 +464,10 @@ do -- MENU_MISSION_COMMAND
-- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this.
-- @return #MENU_MISSION_COMMAND self
function MENU_MISSION_COMMAND:New( MenuText, ParentMenu, CommandMenuFunction, ... )
-
+
MENU_INDEX:PrepareMission()
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
- local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
+ local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
if MissionMenu then
MissionMenu:SetCommandMenuFunction( CommandMenuFunction )
MissionMenu:SetCommandMenuArguments( arg )
@@ -465,7 +475,7 @@ do -- MENU_MISSION_COMMAND
else
local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
MENU_INDEX:SetMissionMenu( Path, self )
-
+
self.MenuPath = missionCommands.addCommand( MenuText, self.MenuParentPath, self.MenuCallHandler )
self:SetParentMenu( self.MenuText, self )
return self
@@ -481,15 +491,15 @@ do -- MENU_MISSION_COMMAND
end
return self
end
-
+
--- Removes a radio command item for a coalition
-- @param #MENU_MISSION_COMMAND self
-- @return #nil
function MENU_MISSION_COMMAND:Remove()
-
+
MENU_INDEX:PrepareMission()
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
+ local MissionMenu = MENU_INDEX:HasMissionMenu( Path )
if MissionMenu == self then
if not MenuStamp or self.MenuStamp ~= MenuStamp then
if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
@@ -505,19 +515,20 @@ do -- MENU_MISSION_COMMAND
else
BASE:E( { "Cannot Remove MENU_MISSION_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText } )
end
-
+
return self
end
end
-do -- MENU_COALITION
- --- @type MENU_COALITION
+do
+ --- MENU_COALITION
+ -- @type MENU_COALITION
-- @extends Core.Menu#MENU_BASE
-
+
--- Manages the main menus for DCS.coalition.
- --
+ --
-- You can add menus with the @{#MENU_COALITION.New} method, which constructs a MENU_COALITION object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION.Remove}.
- --
+ --
--
-- @usage
-- -- This demo creates a menu structure for the planes within the red coalition.
@@ -547,7 +558,7 @@ do -- MENU_COALITION
-- end
--
-- local function AddStatusMenu()
- --
+ --
-- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object.
-- MenuStatus = MENU_COALITION:New( coalition.side.RED, "Status for Planes" )
-- MenuStatusShow = MENU_COALITION_COMMAND:New( coalition.side.RED, "Show Status", MenuStatus, ShowStatus, "Status of planes is ok!", "Message to Red Coalition" )
@@ -555,12 +566,12 @@ do -- MENU_COALITION
--
-- local MenuAdd = MENU_COALITION_COMMAND:New( coalition.side.RED, "Add Status Menu", MenuCoalitionRed, AddStatusMenu )
-- local MenuRemove = MENU_COALITION_COMMAND:New( coalition.side.RED, "Remove Status Menu", MenuCoalitionRed, RemoveStatusMenu )
- --
+ --
-- @field #MENU_COALITION
MENU_COALITION = {
ClassName = "MENU_COALITION"
}
-
+
--- MENU_COALITION constructor. Creates a new MENU_COALITION object and creates the menu for a complete coalition.
-- @param #MENU_COALITION self
-- @param DCS#coalition.side Coalition The coalition owning the menu.
@@ -570,15 +581,15 @@ do -- MENU_COALITION
function MENU_COALITION:New( Coalition, MenuText, ParentMenu )
MENU_INDEX:PrepareCoalition( Coalition )
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
- local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path )
+ local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path )
if CoalitionMenu then
return CoalitionMenu
else
local self = BASE:Inherit( self, MENU_BASE:New( MenuText, ParentMenu ) )
MENU_INDEX:SetCoalitionMenu( Coalition, Path, self )
-
+
self.Coalition = Coalition
-
+
self.MenuPath = missionCommands.addSubMenuForCoalition( Coalition, MenuText, self.MenuParentPath )
self:SetParentMenu( self.MenuText, self )
return self
@@ -594,27 +605,27 @@ do -- MENU_COALITION
end
return self
end
-
+
--- Removes the sub menus recursively of this MENU_COALITION. Note that the main menu is kept!
-- @param #MENU_COALITION self
-- @return #MENU_COALITION
function MENU_COALITION:RemoveSubMenus()
-
+
for MenuID, Menu in pairs( self.Menus or {} ) do
Menu:Remove()
end
-
+
self.Menus = nil
end
-
+
--- Removes the main menu and the sub menus recursively of this MENU_COALITION.
-- @param #MENU_COALITION self
-- @return #nil
function MENU_COALITION:Remove( MenuStamp, MenuTag )
-
+
MENU_INDEX:PrepareCoalition( self.Coalition )
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path )
+ local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path )
if CoalitionMenu == self then
self:RemoveSubMenus()
if not MenuStamp or self.MenuStamp ~= MenuStamp then
@@ -631,17 +642,18 @@ do -- MENU_COALITION
else
BASE:E( { "Cannot Remove MENU_COALITION", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Coalition = self.Coalition } )
end
-
+
return self
end
end
-do -- MENU_COALITION_COMMAND
-
- --- @type MENU_COALITION_COMMAND
+do
+
+ --- MENU_COALITION_COMMAND
+ -- @type MENU_COALITION_COMMAND
-- @extends Core.Menu#MENU_COMMAND_BASE
-
- --- Manages the command menus for coalitions, which allow players to execute functions during mission execution.
- --
+
+ --- Manages the command menus for coalitions, which allow players to execute functions during mission execution.
+ --
-- You can add menus with the @{#MENU_COALITION_COMMAND.New} method, which constructs a MENU_COALITION_COMMAND object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_COALITION_COMMAND.Remove}.
--
@@ -649,7 +661,7 @@ do -- MENU_COALITION_COMMAND
MENU_COALITION_COMMAND = {
ClassName = "MENU_COALITION_COMMAND"
}
-
+
--- MENU_COALITION constructor. Creates a new radio command item for a coalition, which can invoke a function with parameters.
-- @param #MENU_COALITION_COMMAND self
-- @param DCS#coalition.side Coalition The coalition owning the menu.
@@ -659,19 +671,19 @@ do -- MENU_COALITION_COMMAND
-- @param CommandMenuArgument An argument for the function. There can only be ONE argument given. So multiple arguments must be wrapped into a table. See the below example how to do this.
-- @return #MENU_COALITION_COMMAND
function MENU_COALITION_COMMAND:New( Coalition, MenuText, ParentMenu, CommandMenuFunction, ... )
-
+
MENU_INDEX:PrepareCoalition( Coalition )
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
- local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path )
+ local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( Coalition, Path )
if CoalitionMenu then
CoalitionMenu:SetCommandMenuFunction( CommandMenuFunction )
CoalitionMenu:SetCommandMenuArguments( arg )
return CoalitionMenu
else
-
+
local self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
MENU_INDEX:SetCoalitionMenu( Coalition, Path, self )
-
+
self.Coalition = Coalition
self.MenuPath = missionCommands.addCommandForCoalition( self.Coalition, MenuText, self.MenuParentPath, self.MenuCallHandler )
self:SetParentMenu( self.MenuText, self )
@@ -686,18 +698,18 @@ do -- MENU_COALITION_COMMAND
missionCommands.removeItemForCoalition( self.Coalition, self.MenuPath )
missionCommands.addCommandForCoalition( self.Coalition, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
end
-
+
return self
end
-
+
--- Removes a radio command item for a coalition
-- @param #MENU_COALITION_COMMAND self
-- @return #nil
function MENU_COALITION_COMMAND:Remove( MenuStamp, MenuTag )
-
+
MENU_INDEX:PrepareCoalition( self.Coalition )
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path )
+ local CoalitionMenu = MENU_INDEX:HasCoalitionMenu( self.Coalition, Path )
if CoalitionMenu == self then
if not MenuStamp or self.MenuStamp ~= MenuStamp then
if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
@@ -713,7 +725,7 @@ do -- MENU_COALITION_COMMAND
else
BASE:E( { "Cannot Remove MENU_COALITION_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Coalition = self.Coalition } )
end
-
+
return self
end
end
@@ -725,23 +737,26 @@ do
-- So every menu for a client created must be tracked so that program logic accidentally does not create.
-- the same menus twice during initialization logic.
-- These menu classes are handling this logic with this variable.
+
local _MENUGROUPS = {}
- --- @type MENU_GROUP
+
+ ---
+ -- @type MENU_GROUP
-- @extends Core.Menu#MENU_BASE
-
-
- --- Manages the main menus for @{Wrapper.Group}s.
- --
+
+
+ --- Manages the main menus for @{Wrapper.Group}s.
+ --
-- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}.
- --
+ --
-- @usage
-- -- This demo creates a menu structure for the two groups of planes.
-- -- Each group will receive a different menu structure.
-- -- To test, join the planes, then look at the other radio menus (Option F10).
-- -- Then switch planes and check if the menu is still there.
-- -- And play with the Add and Remove menu options.
- --
+ --
-- -- Note that in multi player, this will only work after the DCS groups bug is solved.
--
-- local function ShowStatus( PlaneGroup, StatusText, Coalition )
@@ -757,7 +772,7 @@ do
-- MenuStatus[MenuGroupName]:Remove()
-- end
--
- -- --- @param Wrapper.Group#GROUP MenuGroup
+ -- -- @param Wrapper.Group#GROUP MenuGroup
-- local function AddStatusMenu( MenuGroup )
-- local MenuGroupName = MenuGroup:GetName()
-- -- This would create a menu for the red coalition under the MenuCoalitionRed menu object.
@@ -789,7 +804,7 @@ do
MENU_GROUP = {
ClassName = "MENU_GROUP"
}
-
+
--- MENU_GROUP constructor. Creates a new radio menu item for a group.
-- @param #MENU_GROUP self
-- @param Wrapper.Group#GROUP Group The Group owning the menu.
@@ -797,7 +812,7 @@ do
-- @param #table ParentMenu The parent menu.
-- @return #MENU_GROUP self
function MENU_GROUP:New( Group, MenuText, ParentMenu )
-
+
MENU_INDEX:PrepareGroup( Group )
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
@@ -809,13 +824,13 @@ do
self.Group = Group
self.GroupID = Group:GetID()
self.MenuPath = missionCommands.addSubMenuForGroup( self.GroupID, MenuText, self.MenuParentPath )
-
+
self:SetParentMenu( self.MenuText, self )
return self
end
-
+
end
-
+
--- Refreshes a new radio item for a group and submenus
-- @param #MENU_GROUP self
-- @return #MENU_GROUP
@@ -823,15 +838,15 @@ do
do
missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )
-
+
for MenuText, Menu in pairs( self.Menus or {} ) do
Menu:Refresh()
end
end
-
+
return self
end
-
+
--- Refreshes a new radio item for a group and submenus, ordering by (numerical) MenuTag
-- @param #MENU_GROUP self
-- @return #MENU_GROUP
@@ -840,7 +855,7 @@ do
do
missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )
-
+
local MenuTable = {}
for MenuText, Menu in pairs( self.Menus or {} ) do
local tag = Menu.MenuTag or math.random(1,10000)
@@ -849,12 +864,12 @@ do
table.sort(MenuTable, function (k1, k2) return k1.tag < k2.tag end )
for _, Menu in pairs( MenuTable ) do
Menu.Entry:Refresh()
- end
+ end
end
-
+
return self
end
-
+
--- Removes the sub menus recursively of this MENU_GROUP.
-- @param #MENU_GROUP self
-- @param MenuStamp
@@ -864,9 +879,9 @@ do
for MenuText, Menu in pairs( self.Menus or {} ) do
Menu:Remove( MenuStamp, MenuTag )
end
-
+
self.Menus = nil
-
+
end
--- Removes the main menu and sub menus recursively of this MENU_GROUP.
@@ -877,7 +892,7 @@ do
function MENU_GROUP:Remove( MenuStamp, MenuTag )
MENU_INDEX:PrepareGroup( self.Group )
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
+ local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
if GroupMenu == self then
self:RemoveSubMenus( MenuStamp, MenuTag )
if not MenuStamp or self.MenuStamp ~= MenuStamp then
@@ -895,15 +910,15 @@ do
BASE:E( { "Cannot Remove MENU_GROUP", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
return nil
end
-
+
return self
end
-
- --- @type MENU_GROUP_COMMAND
+ ---
+ -- @type MENU_GROUP_COMMAND
-- @extends Core.Menu#MENU_COMMAND_BASE
-
- --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution.
+
+ --- The @{Core.Menu#MENU_GROUP_COMMAND} class manages the command menus for coalitions, which allow players to execute functions during mission execution.
-- You can add menus with the @{#MENU_GROUP_COMMAND.New} method, which constructs a MENU_GROUP_COMMAND object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND.Remove}.
--
@@ -911,7 +926,7 @@ do
MENU_GROUP_COMMAND = {
ClassName = "MENU_GROUP_COMMAND"
}
-
+
--- Creates a new radio command item for a group
-- @param #MENU_GROUP_COMMAND self
-- @param Wrapper.Group#GROUP Group The Group owning the menu.
@@ -923,7 +938,7 @@ do
function MENU_GROUP_COMMAND:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... )
MENU_INDEX:PrepareGroup( Group )
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
- local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
+ local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
if GroupMenu then
GroupMenu:SetCommandMenuFunction( CommandMenuFunction )
GroupMenu:SetCommandMenuArguments( arg )
@@ -931,12 +946,12 @@ do
else
self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
MENU_INDEX:SetGroupMenu( Group, Path, self )
-
+
self.Group = Group
self.GroupID = Group:GetID()
-
+
self.MenuPath = missionCommands.addCommandForGroup( self.GroupID, MenuText, self.MenuParentPath, self.MenuCallHandler )
-
+
self:SetParentMenu( self.MenuText, self )
return self
end
@@ -949,10 +964,10 @@ do
missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
end
-
+
return self
end
-
+
--- Removes a menu structure for a group.
-- @param #MENU_GROUP_COMMAND self
-- @param MenuStamp
@@ -961,7 +976,7 @@ do
function MENU_GROUP_COMMAND:Remove( MenuStamp, MenuTag )
MENU_INDEX:PrepareGroup( self.Group )
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
+ local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
if GroupMenu == self then
if not MenuStamp or self.MenuStamp ~= MenuStamp then
if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
@@ -977,28 +992,29 @@ do
else
BASE:E( { "Cannot Remove MENU_GROUP_COMMAND", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
end
-
+
return self
end
end
--- MENU_GROUP_DELAYED
do
- --- @type MENU_GROUP_DELAYED
+ ---
+ -- @type MENU_GROUP_DELAYED
-- @extends Core.Menu#MENU_BASE
-
-
- --- The MENU_GROUP_DELAYED class manages the main menus for groups.
+
+
+ --- The MENU_GROUP_DELAYED class manages the main menus for groups.
-- You can add menus with the @{#MENU_GROUP.New} method, which constructs a MENU_GROUP object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP.Remove}.
-- The creation of the menu item is delayed however, and must be created using the @{#MENU_GROUP.Set} method.
-- This method is most of the time called after the "old" menu items have been removed from the sub menu.
- --
+ --
--
-- @field #MENU_GROUP_DELAYED
MENU_GROUP_DELAYED = {
ClassName = "MENU_GROUP_DELAYED"
}
-
+
--- MENU_GROUP_DELAYED constructor. Creates a new radio menu item for a group.
-- @param #MENU_GROUP_DELAYED self
-- @param Wrapper.Group#GROUP Group The Group owning the menu.
@@ -1006,7 +1022,7 @@ do
-- @param #table ParentMenu The parent menu.
-- @return #MENU_GROUP_DELAYED self
function MENU_GROUP_DELAYED:New( Group, MenuText, ParentMenu )
-
+
MENU_INDEX:PrepareGroup( Group )
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
@@ -1023,23 +1039,24 @@ do
self.MenuPath = {}
end
table.insert( self.MenuPath, self.MenuText )
-
+
self:SetParentMenu( self.MenuText, self )
return self
end
-
+
end
--- Refreshes a new radio item for a group and submenus
-- @param #MENU_GROUP_DELAYED self
-- @return #MENU_GROUP_DELAYED
function MENU_GROUP_DELAYED:Set()
+ if not self.GroupID then return end
do
if not self.MenuSet then
missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )
self.MenuSet = true
end
-
+
for MenuText, Menu in pairs( self.Menus or {} ) do
Menu:Set()
end
@@ -1053,15 +1070,15 @@ do
do
missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
missionCommands.addSubMenuForGroup( self.GroupID, self.MenuText, self.MenuParentPath )
-
+
for MenuText, Menu in pairs( self.Menus or {} ) do
Menu:Refresh()
end
end
-
+
return self
end
-
+
--- Removes the sub menus recursively of this MENU_GROUP_DELAYED.
-- @param #MENU_GROUP_DELAYED self
-- @param MenuStamp
@@ -1071,9 +1088,9 @@ do
for MenuText, Menu in pairs( self.Menus or {} ) do
Menu:Remove( MenuStamp, MenuTag )
end
-
+
self.Menus = nil
-
+
end
--- Removes the main menu and sub menus recursively of this MENU_GROUP.
@@ -1084,7 +1101,7 @@ do
function MENU_GROUP_DELAYED:Remove( MenuStamp, MenuTag )
MENU_INDEX:PrepareGroup( self.Group )
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
+ local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
if GroupMenu == self then
self:RemoveSubMenus( MenuStamp, MenuTag )
if not MenuStamp or self.MenuStamp ~= MenuStamp then
@@ -1102,16 +1119,16 @@ do
BASE:E( { "Cannot Remove MENU_GROUP_DELAYED", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
return nil
end
-
+
return self
end
-
- --- @type MENU_GROUP_COMMAND_DELAYED
+ ---
+ -- @type MENU_GROUP_COMMAND_DELAYED
-- @extends Core.Menu#MENU_COMMAND_BASE
-
- --- Manages the command menus for coalitions, which allow players to execute functions during mission execution.
- --
+
+ --- Manages the command menus for coalitions, which allow players to execute functions during mission execution.
+ --
-- You can add menus with the @{#MENU_GROUP_COMMAND_DELAYED.New} method, which constructs a MENU_GROUP_COMMAND_DELAYED object and returns you the object reference.
-- Using this object reference, you can then remove ALL the menus and submenus underlying automatically with @{#MENU_GROUP_COMMAND_DELAYED.Remove}.
--
@@ -1119,7 +1136,7 @@ do
MENU_GROUP_COMMAND_DELAYED = {
ClassName = "MENU_GROUP_COMMAND_DELAYED"
}
-
+
--- Creates a new radio command item for a group
-- @param #MENU_GROUP_COMMAND_DELAYED self
-- @param Wrapper.Group#GROUP Group The Group owning the menu.
@@ -1131,7 +1148,7 @@ do
function MENU_GROUP_COMMAND_DELAYED:New( Group, MenuText, ParentMenu, CommandMenuFunction, ... )
MENU_INDEX:PrepareGroup( Group )
local Path = MENU_INDEX:ParentPath( ParentMenu, MenuText )
- local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
+ local GroupMenu = MENU_INDEX:HasGroupMenu( Group, Path )
if GroupMenu then
GroupMenu:SetCommandMenuFunction( CommandMenuFunction )
GroupMenu:SetCommandMenuArguments( arg )
@@ -1139,17 +1156,17 @@ do
else
self = BASE:Inherit( self, MENU_COMMAND_BASE:New( MenuText, ParentMenu, CommandMenuFunction, arg ) )
MENU_INDEX:SetGroupMenu( Group, Path, self )
-
+
self.Group = Group
self.GroupID = Group:GetID()
-
+
if self.MenuParentPath then
self.MenuPath = UTILS.DeepCopy( self.MenuParentPath )
else
self.MenuPath = {}
end
table.insert( self.MenuPath, self.MenuText )
-
+
self:SetParentMenu( self.MenuText, self )
return self
end
@@ -1165,7 +1182,7 @@ do
end
end
end
-
+
--- Refreshes a radio item for a group
-- @param #MENU_GROUP_COMMAND_DELAYED self
-- @return #MENU_GROUP_COMMAND_DELAYED
@@ -1174,10 +1191,10 @@ do
missionCommands.removeItemForGroup( self.GroupID, self.MenuPath )
missionCommands.addCommandForGroup( self.GroupID, self.MenuText, self.MenuParentPath, self.MenuCallHandler )
end
-
+
return self
end
-
+
--- Removes a menu structure for a group.
-- @param #MENU_GROUP_COMMAND_DELAYED self
-- @param MenuStamp
@@ -1186,7 +1203,7 @@ do
function MENU_GROUP_COMMAND_DELAYED:Remove( MenuStamp, MenuTag )
MENU_INDEX:PrepareGroup( self.Group )
local Path = MENU_INDEX:ParentPath( self.ParentMenu, self.MenuText )
- local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
+ local GroupMenu = MENU_INDEX:HasGroupMenu( self.Group, Path )
if GroupMenu == self then
if not MenuStamp or self.MenuStamp ~= MenuStamp then
if ( not MenuTag ) or ( MenuTag and self.MenuTag and MenuTag == self.MenuTag ) then
@@ -1202,7 +1219,7 @@ do
else
BASE:E( { "Cannot Remove MENU_GROUP_COMMAND_DELAYED", Path = Path, ParentMenu = self.ParentMenu, MenuText = self.MenuText, Group = self.Group } )
end
-
+
return self
end
end
diff --git a/Moose Development/Moose/Core/Message.lua b/Moose Development/Moose/Core/Message.lua
index 6c15c5dc3..0a3defdec 100644
--- a/Moose Development/Moose/Core/Message.lua
+++ b/Moose Development/Moose/Core/Message.lua
@@ -516,10 +516,10 @@ function MESSAGE.SetMSRS(PathToSRS,Port,PathToCredentials,Frequency,Modulation,G
end
_MESSAGESRS.label = Label or MSRS.Label or "MESSAGE"
- _MESSAGESRS.MSRS:SetLabel(Label or "MESSAGE")
+ _MESSAGESRS.MSRS:SetLabel(_MESSAGESRS.label)
_MESSAGESRS.port = Port or MSRS.port or 5002
- _MESSAGESRS.MSRS:SetPort(Port or 5002)
+ _MESSAGESRS.MSRS:SetPort(_MESSAGESRS.port)
_MESSAGESRS.volume = Volume or MSRS.volume or 1
_MESSAGESRS.MSRS:SetVolume(_MESSAGESRS.volume)
diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua
index ce3a11827..43812c1e1 100644
--- a/Moose Development/Moose/Core/Set.lua
+++ b/Moose Development/Moose/Core/Set.lua
@@ -1516,6 +1516,7 @@ do
self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
+ self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnDeadOrCrash )
if self.Filter.Zones then
self.ZoneTimer = TIMER:New(self._ContinousZoneFilter,self)
local timing = self.ZoneTimerInterval or 30
@@ -3477,7 +3478,7 @@ do -- SET_STATIC
--- Add STATIC(s) to SET_STATIC.
-- @param #SET_STATIC self
- -- @param #string AddStatic A single STATIC.
+ -- @param Wrapper.Static#STATIC AddStatic A single STATIC.
-- @return #SET_STATIC self
function SET_STATIC:AddStatic( AddStatic )
self:F2( AddStatic:GetName() )
diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua
index 36a375275..75664cee9 100644
--- a/Moose Development/Moose/Core/Spawn.lua
+++ b/Moose Development/Moose/Core/Spawn.lua
@@ -202,19 +202,19 @@
--
-- ### Link-16 Datalink STN and SADL IDs (limited at the moment to F15/16/18/AWACS/Tanker/B1B, but not the F15E for clients, SADL A10CII only)
--
--- *{#SPAWN.InitSTN}(): Set the STN of the first unit in the group. All other units will have consecutive STNs, provided they have not been used yet.
--- *{#SPAWN.InitSADL}(): Set the SADL of the first unit in the group. All other units will have consecutive SADLs, provided they have not been used yet.
+-- * @{#SPAWN.InitSTN}(): Set the STN of the first unit in the group. All other units will have consecutive STNs, provided they have not been used yet.
+-- * @{#SPAWN.InitSADL}(): Set the SADL of the first unit in the group. All other units will have consecutive SADLs, provided they have not been used yet.
--
-- ### Callsigns
--
--- *{#SPAWN.InitRandomizeCallsign}(): Set a random callsign name per spawn.
--- *{#SPAWN.SpawnInitCallSign}(): Set a specific callsign for a spawned group.
+-- * @{#SPAWN.InitRandomizeCallsign}(): Set a random callsign name per spawn.
+-- * @{#SPAWN.SpawnInitCallSign}(): Set a specific callsign for a spawned group.
--
-- ### Speed
--
--- *{#SPAWN.InitSpeedMps}(): Set the initial speed on spawning in meters per second.
--- *{#SPAWN.InitSpeedKph}(): Set the initial speed on spawning in kilometers per hour.
--- *{#SPAWN.InitSpeedKnots}(): Set the initial speed on spawning in knots.
+-- * @{#SPAWN.InitSpeedMps}(): Set the initial speed on spawning in meters per second.
+-- * @{#SPAWN.InitSpeedKph}(): Set the initial speed on spawning in kilometers per hour.
+-- * @{#SPAWN.InitSpeedKnots}(): Set the initial speed on spawning in knots.
--
-- ## SPAWN **Spawn** methods
--
@@ -620,12 +620,14 @@ end
-- and any spaces before and after the resulting name are removed.
-- IMPORTANT! This method MUST be the first used after :New !!!
-- @param #SPAWN self
--- @param #boolean KeepUnitNames (optional) If true, the unit names are kept, false or not provided to make new unit names.
+-- @param #boolean KeepUnitNames (optional) If true, the unit names are kept, false or not provided create new unit names.
-- @return #SPAWN self
function SPAWN:InitKeepUnitNames( KeepUnitNames )
self:F()
- self.SpawnInitKeepUnitNames = KeepUnitNames or true
+ self.SpawnInitKeepUnitNames = false
+
+ if KeepUnitNames == true then self.SpawnInitKeepUnitNames = true end
return self
end
@@ -1209,11 +1211,12 @@ end
-- @param #number Major Major number, i.e. the group number of this name, e.g. 1 - resulting in e.g. Texaco-2-1
-- @return #SPAWN self
function SPAWN:InitCallSign(ID,Name,Minor,Major)
+ local Name = Name or "Enfield"
self.SpawnInitCallSign = true
self.SpawnInitCallSignID = ID or 1
self.SpawnInitCallSignMinor = Minor or 1
self.SpawnInitCallSignMajor = Major or 1
- self.SpawnInitCallSignName = string.lower(Name) or "enfield"
+ self.SpawnInitCallSignName=string.lower(Name):gsub("^%l", string.upper)
return self
end
@@ -1609,8 +1612,8 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth )
RandomVec2 = PointVec3:GetRandomVec2InRadius( self.SpawnOuterRadius, self.SpawnInnerRadius )
numTries = numTries + 1
inZone = SpawnZone:IsVec2InZone(RandomVec2)
- self:I("Retrying " .. numTries .. "spawn " .. SpawnTemplate.name .. " in Zone " .. SpawnZone:GetName() .. "!")
- self:I(SpawnZone)
+ --self:I("Retrying " .. numTries .. "spawn " .. SpawnTemplate.name .. " in Zone " .. SpawnZone:GetName() .. "!")
+ --self:I(SpawnZone)
end
end
if (not inZone) then
@@ -3436,24 +3439,28 @@ function SPAWN:_Prepare( SpawnTemplatePrefix, SpawnIndex ) -- R2.2
end
if self.SpawnInitKeepUnitNames == false then
- for UnitID = 1, #SpawnTemplate.units do
- SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID )
+ for UnitID = 1, #SpawnTemplate.units do
+ if not string.find(SpawnTemplate.units[UnitID].name,"#IFF_",1,true) then --Razbam IFF hack for F15E etc
+ SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID )
+ end
SpawnTemplate.units[UnitID].unitId = nil
end
else
for UnitID = 1, #SpawnTemplate.units do
- local SpawnInitKeepUnitIFF = false
- if string.find(SpawnTemplate.units[UnitID].name,"#IFF_",1,true) then --Razbam IFF hack for F15E etc
- SpawnInitKeepUnitIFF = true
- end
- local UnitPrefix, Rest
- if SpawnInitKeepUnitIFF == false then
- UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" )
- self:T( { UnitPrefix, Rest } )
- else
- UnitPrefix=SpawnTemplate.units[UnitID].name
+ local SpawnInitKeepUnitIFF = false
+ if string.find(SpawnTemplate.units[UnitID].name,"#IFF_",1,true) then --Razbam IFF hack for F15E etc
+ SpawnInitKeepUnitIFF = true
end
- SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID )
+ local UnitPrefix, Rest
+ if SpawnInitKeepUnitIFF == false then
+ UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" )
+ SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID )
+ self:T( { UnitPrefix, Rest } )
+ --else
+ --UnitPrefix=SpawnTemplate.units[UnitID].name
+ end
+ --SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID )
+
SpawnTemplate.units[UnitID].unitId = nil
end
end
diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua
index 786b06b80..a809efe97 100644
--- a/Moose Development/Moose/Core/Zone.lua
+++ b/Moose Development/Moose/Core/Zone.lua
@@ -3090,9 +3090,25 @@ function ZONE_POLYGON:NewFromDrawing(DrawingName)
-- points for the drawings are saved in local space, so add the object's map x and y coordinates to get
-- world space points we can use
for _, point in UTILS.spairs(object["points"]) do
+ -- check if we want to skip adding a point
+ local skip = false
local p = {x = object["mapX"] + point["x"],
y = object["mapY"] + point["y"] }
- table.add(points, p)
+
+ -- Check if the same coordinates already exist in the list, skip if they do
+ -- This can happen when drawing a Polygon in Free mode, DCS adds points on
+ -- top of each other that are in the `mission` file, but not visible in the
+ -- Mission Editor
+ for _, pt in pairs(points) do
+ if pt.x == p.x and pt.y == p.y then
+ skip = true
+ end
+ end
+
+ -- if it's a unique point, add it
+ if not skip then
+ table.add(points, p)
+ end
end
elseif object["polygonMode"] == "rect" then
-- the points for a rect are saved as local coordinates with an angle. To get the world space points from this
@@ -3110,6 +3126,7 @@ function ZONE_POLYGON:NewFromDrawing(DrawingName)
points = {p1, p2, p3, p4}
else
+ -- bring the Arrow code over from Shape/Polygon
-- something else that might be added in the future
end
end
diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua
index 0f6a8e52d..612162389 100644
--- a/Moose Development/Moose/Functional/Autolase.lua
+++ b/Moose Development/Moose/Functional/Autolase.lua
@@ -74,7 +74,7 @@
-- @image Designation.JPG
--
-- Date: 24 Oct 2021
--- Last Update: Jan 2024
+-- Last Update: May 2024
--
--- Class AUTOLASE
-- @type AUTOLASE
@@ -88,6 +88,7 @@
-- @field #table LaserCodes
-- @field #table playermenus
-- @field #boolean smokemenu
+-- @field #boolean threatmenu
-- @extends Ops.Intel#INTEL
---
@@ -117,7 +118,7 @@ AUTOLASE = {
--- AUTOLASE class version.
-- @field #string version
-AUTOLASE.version = "0.1.23"
+AUTOLASE.version = "0.1.25"
-------------------------------------------------------------------
-- Begin Functional.Autolase.lua
@@ -205,6 +206,7 @@ function AUTOLASE:New(RecceSet, Coalition, Alias, PilotSet)
self:SetLaserCodes( { 1688, 1130, 4785, 6547, 1465, 4578 } ) -- set self.LaserCodes
self.playermenus = {}
self.smokemenu = true
+ self.threatmenu = true
-- Set some string id for output to DCS.log file.
self.lid=string.format("AUTOLASE %s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
@@ -323,39 +325,51 @@ end
function AUTOLASE:SetPilotMenu()
if self.usepilotset then
local pilottable = self.pilotset:GetSetObjects() or {}
+ local grouptable = {}
for _,_unit in pairs (pilottable) do
local Unit = _unit -- Wrapper.Unit#UNIT
if Unit and Unit:IsAlive() then
local Group = Unit:GetGroup()
+ local GroupName = Group:GetName() or "none"
local unitname = Unit:GetName()
- if self.playermenus[unitname] then self.playermenus[unitname]:Remove() end
- local lasetopm = MENU_GROUP:New(Group,"Autolase",nil)
- self.playermenus[unitname] = lasetopm
- local lasemenu = MENU_GROUP_COMMAND:New(Group,"Status",lasetopm,self.ShowStatus,self,Group,Unit)
- if self.smokemenu then
- local smoke = (self.smoketargets == true) and "off" or "on"
- local smoketext = string.format("Switch smoke targets to %s",smoke)
- local smokemenu = MENU_GROUP_COMMAND:New(Group,smoketext,lasetopm,self.SetSmokeTargets,self,(not self.smoketargets))
- end
- for _,_grp in pairs(self.RecceSet.Set) do
- local grp = _grp -- Wrapper.Group#GROUP
- local unit = grp:GetUnit(1)
- --local name = grp:GetName()
- if unit and unit:IsAlive() then
- local name = unit:GetName()
- local mname = string.gsub(name,".%d+.%d+$","")
- local code = self:GetLaserCode(name)
- local unittop = MENU_GROUP:New(Group,"Change laser code for "..mname,lasetopm)
- for _,_code in pairs(self.LaserCodes) do
- local text = tostring(_code)
- if _code == code then text = text.."(*)" end
- local changemenu = MENU_GROUP_COMMAND:New(Group,text,unittop,self.SetRecceLaserCode,self,name,_code,true)
- end
- end
- end
+ if not grouptable[GroupName] == true then
+ if self.playermenus[unitname] then self.playermenus[unitname]:Remove() end -- menus
+ local lasetopm = MENU_GROUP:New(Group,"Autolase",nil)
+ self.playermenus[unitname] = lasetopm
+ local lasemenu = MENU_GROUP_COMMAND:New(Group,"Status",lasetopm,self.ShowStatus,self,Group,Unit)
+ if self.smokemenu then
+ local smoke = (self.smoketargets == true) and "off" or "on"
+ local smoketext = string.format("Switch smoke targets to %s",smoke)
+ local smokemenu = MENU_GROUP_COMMAND:New(Group,smoketext,lasetopm,self.SetSmokeTargets,self,(not self.smoketargets))
+ end -- smokement
+ if self.threatmenu then
+ local threatmenutop = MENU_GROUP:New(Group,"Set min lasing threat",lasetopm)
+ for i=0,10,2 do
+ local text = "Threatlevel "..tostring(i)
+ local threatmenu = MENU_GROUP_COMMAND:New(Group,text,threatmenutop,self.SetMinThreatLevel,self,i)
+ end -- threatlevel
+ end -- threatmenu
+ for _,_grp in pairs(self.RecceSet.Set) do
+ local grp = _grp -- Wrapper.Group#GROUP
+ local unit = grp:GetUnit(1)
+ --local name = grp:GetName()
+ if unit and unit:IsAlive() then
+ local name = unit:GetName()
+ local mname = string.gsub(name,".%d+.%d+$","")
+ local code = self:GetLaserCode(name)
+ local unittop = MENU_GROUP:New(Group,"Change laser code for "..mname,lasetopm)
+ for _,_code in pairs(self.LaserCodes) do
+ local text = tostring(_code)
+ if _code == code then text = text.."(*)" end
+ local changemenu = MENU_GROUP_COMMAND:New(Group,text,unittop,self.SetRecceLaserCode,self,name,_code,true)
+ end -- Codes
+ end -- unit alive
+ end -- Recceset
+ grouptable[GroupName] = true
+ end -- grouptable[GroupName]
--lasemenu:Refresh()
- end
- end
+ end -- unit alive
+ end -- pilot loop
else
if not self.NoMenus then
self.Menu = MENU_COALITION_COMMAND:New(self.coalition,"Autolase",nil,self.ShowStatus,self)
@@ -602,6 +616,21 @@ function AUTOLASE:DisableSmokeMenu()
return self
end
+--- (User) Show the "Switch min threat lasing..." menu entry for pilots. On by default.
+-- @param #AUTOLASE self
+-- @return #AUTOLASE self
+function AUTOLASE:EnableThreatLevelMenu()
+ self.threatmenu = true
+ return self
+end
+
+--- (User) Do not show the "Switch min threat lasing..." menu entry for pilots.
+-- @param #AUTOLASE self
+-- @return #AUTOLASE self
+function AUTOLASE:DisableThreatLevelMenu()
+ self.threatmenu = false
+ return self
+end
--- (Internal) Function to calculate line of sight.
-- @param #AUTOLASE self
@@ -730,6 +759,7 @@ function AUTOLASE:ShowStatus(Group,Unit)
report:Add(string.format("Recce %s has code %d",name,code))
end
end
+ report:Add(string.format("Lasing min threat level %d",self.minthreatlevel))
local lines = 0
for _ind,_entry in pairs(self.CurrentLasing) do
local entry = _entry -- #AUTOLASE.LaserSpot
diff --git a/Moose Development/Moose/Functional/Designate.lua b/Moose Development/Moose/Functional/Designate.lua
index 811a865ae..b6156d256 100644
--- a/Moose Development/Moose/Functional/Designate.lua
+++ b/Moose Development/Moose/Functional/Designate.lua
@@ -184,7 +184,7 @@
do -- DESIGNATE
- --- @type DESIGNATE
+ -- @type DESIGNATE
-- @extends Core.Fsm#FSM_PROCESS
--- Manage the designation of detected targets.
@@ -525,7 +525,7 @@ do -- DESIGNATE
self.AttackSet:ForEachGroupAlive(
- --- @param Wrapper.Group#GROUP AttackGroup
+ -- @param Wrapper.Group#GROUP AttackGroup
function( AttackGroup )
self.FlashStatusMenu[AttackGroup] = FlashMenu
end
@@ -554,7 +554,7 @@ do -- DESIGNATE
self.AttackSet:ForEachGroupAlive(
- --- @param Wrapper.Group#GROUP AttackGroup
+ -- @param Wrapper.Group#GROUP AttackGroup
function( AttackGroup )
self.FlashDetectionMessage[AttackGroup] = FlashDetectionMessage
end
@@ -826,7 +826,7 @@ do -- DESIGNATE
-- This Detection is obsolete, remove from the designate scope
self.Designating[DesignateIndex] = nil
self.AttackSet:ForEachGroupAlive(
- --- @param Wrapper.Group#GROUP AttackGroup
+ -- @param Wrapper.Group#GROUP AttackGroup
function( AttackGroup )
if AttackGroup:IsAlive() == true then
local DetectionText = self.Detection:DetectedItemReportSummary( DetectedItem, AttackGroup ):Text( ", " )
@@ -903,7 +903,7 @@ do -- DESIGNATE
self.AttackSet:ForEachGroupAlive(
- --- @param Wrapper.Group#GROUP GroupReport
+ -- @param Wrapper.Group#GROUP GroupReport
function( AttackGroup )
if self.FlashStatusMenu[AttackGroup] or ( MenuAttackGroup and ( AttackGroup:GetName() == MenuAttackGroup:GetName() ) ) then
@@ -1060,7 +1060,7 @@ do -- DESIGNATE
self.AttackSet:ForEachGroupAlive(
- --- @param Wrapper.Group#GROUP GroupReport
+ -- @param Wrapper.Group#GROUP GroupReport
function( AttackGroup )
self:ScheduleOnce( Delay, self.SetMenu, self, AttackGroup )
@@ -1198,7 +1198,7 @@ do -- DESIGNATE
--local ReportTypes = REPORT:New()
--local ReportLaserCodes = REPORT:New()
- TargetSetUnit:Flush( self )
+ --TargetSetUnit:Flush( self )
--self:F( { Recces = self.Recces } )
for TargetUnit, RecceData in pairs( self.Recces ) do
@@ -1229,10 +1229,12 @@ do -- DESIGNATE
end
end
+ if TargetSetUnit == nil then return end
+
if self.AutoLase or ( not self.AutoLase and ( self.LaseStart + Duration >= timer.getTime() ) ) then
TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0,
- --- @param Wrapper.Unit#UNIT SmokeUnit
+ -- @param Wrapper.Unit#UNIT SmokeUnit
function( TargetUnit )
self:F( { TargetUnit = TargetUnit:GetName() } )
@@ -1253,7 +1255,7 @@ do -- DESIGNATE
local RecceUnit = UnitData -- Wrapper.Unit#UNIT
local RecceUnitDesc = RecceUnit:GetDesc()
- --self:F( { RecceUnit = RecceUnit:GetName(), RecceDescription = RecceUnitDesc } )
+ --self:F( { RecceUnit = RecceUnit:GetName(), RecceDescription = RecceUnitDesc } )x
if RecceUnit:IsLasing() == false then
--self:F( { IsDetected = RecceUnit:IsDetected( TargetUnit ), IsLOS = RecceUnit:IsLOS( TargetUnit ) } )
@@ -1275,9 +1277,10 @@ do -- DESIGNATE
local Spot = RecceUnit:LaseUnit( TargetUnit, LaserCode, Duration )
local AttackSet = self.AttackSet
local DesignateName = self.DesignateName
+ local typename = TargetUnit:GetTypeName()
function Spot:OnAfterDestroyed( From, Event, To )
- self.Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() .. " destroyed. " .. TargetSetUnit:Count() .. " targets left.",
+ self.Recce:MessageToSetGroup( "Target " ..typename .. " destroyed. " .. TargetSetUnit:CountAlive() .. " targets left.",
5, AttackSet, self.DesignateName )
end
@@ -1285,7 +1288,7 @@ do -- DESIGNATE
-- OK. We have assigned for the Recce a TargetUnit. We can exit the function.
MarkingCount = MarkingCount + 1
local TargetUnitType = TargetUnit:GetTypeName()
- RecceUnit:MessageToSetGroup( "Marking " .. TargetUnit:GetTypeName() .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.",
+ RecceUnit:MessageToSetGroup( "Marking " .. TargetUnitType .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.",
10, self.AttackSet, DesignateName )
if not MarkedTypes[TargetUnitType] then
MarkedTypes[TargetUnitType] = true
@@ -1392,7 +1395,7 @@ do -- DESIGNATE
local MarkedCount = 0
TargetSetUnit:ForEachUnitPerThreatLevel( 10, 0,
- --- @param Wrapper.Unit#UNIT SmokeUnit
+ -- @param Wrapper.Unit#UNIT SmokeUnit
function( SmokeUnit )
if MarkedCount < self.MaximumMarkings then
@@ -1457,9 +1460,10 @@ do -- DESIGNATE
-- @param #DESIGNATE self
-- @return #DESIGNATE
function DESIGNATE:onafterDoneSmoking( From, Event, To, Index )
-
- self.Designating[Index] = string.gsub( self.Designating[Index], "S", "" )
- self:SetDesignateMenu()
+ if self.Designating[Index] ~= nil then
+ self.Designating[Index] = string.gsub( self.Designating[Index], "S", "" )
+ self:SetDesignateMenu()
+ end
end
--- DoneIlluminating
@@ -1472,5 +1476,3 @@ do -- DESIGNATE
end
end
-
-
diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua
index bc65c4d4f..a6196f861 100644
--- a/Moose Development/Moose/Functional/Detection.lua
+++ b/Moose Development/Moose/Functional/Detection.lua
@@ -545,7 +545,7 @@ do -- DETECTION_BASE
-- @param #string To The To State string.
function DETECTION_BASE:onafterDetect( From, Event, To )
- local DetectDelay = 0.1
+ local DetectDelay = 0.15
self.DetectionCount = 0
self.DetectionRun = 0
self:UnIdentifyAllDetectedObjects() -- Resets the DetectedObjectsIdentified table
@@ -604,7 +604,7 @@ do -- DETECTION_BASE
-- @param #number DetectionTimeStamp Time stamp of detection event.
function DETECTION_BASE:onafterDetection( From, Event, To, Detection, DetectionTimeStamp )
- -- self:F( { DetectedObjects = self.DetectedObjects } )
+ self:I( { DetectedObjects = self.DetectedObjects } )
self.DetectionRun = self.DetectionRun + 1
@@ -612,14 +612,14 @@ do -- DETECTION_BASE
if Detection and Detection:IsAlive() then
- -- self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } )
+ self:I( { "DetectionGroup is Alive", Detection:GetName() } )
local DetectionGroupName = Detection:GetName()
local DetectionUnit = Detection:GetUnit( 1 )
local DetectedUnits = {}
- local DetectedTargets = Detection:GetDetectedTargets(
+ local DetectedTargets = DetectionUnit:GetDetectedTargets(
self.DetectVisual,
self.DetectOptical,
self.DetectRadar,
@@ -628,8 +628,10 @@ do -- DETECTION_BASE
self.DetectDLINK
)
- self:F( { DetectedTargets = DetectedTargets } )
-
+ --self:I( { DetectedTargets = DetectedTargets } )
+ --self:I(UTILS.PrintTableToLog(DetectedTargets))
+
+
for DetectionObjectID, Detection in pairs( DetectedTargets ) do
local DetectedObject = Detection.object -- DCS#Object
diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua
index 3e2f57477..b6d711964 100644
--- a/Moose Development/Moose/Functional/Mantis.lua
+++ b/Moose Development/Moose/Functional/Mantis.lua
@@ -22,7 +22,7 @@
-- @module Functional.Mantis
-- @image Functional.Mantis.jpg
--
--- Last Update: Feb 2024
+-- Last Update: May 2024
-------------------------------------------------------------------------
--- **MANTIS** class, extends Core.Base#BASE
@@ -58,6 +58,7 @@
-- @field #boolean ShoradLink If true, #MANTIS has #SHORAD enabled
-- @field #number ShoradTime Timer in seconds, how long #SHORAD will be active after a detection inside of the defense range
-- @field #number ShoradActDistance Distance of an attacker in meters from a Mantis SAM site, on which Shorad will be switched on. Useful to not give away Shorad sites too early. Default 15km. Should be smaller than checkradius.
+-- @field #boolean checkforfriendlies If true, do not activate a SAM installation if a friendly aircraft is in firing range.
-- @extends Core.Base#BASE
@@ -187,29 +188,34 @@
-- -- This is effectively a 3-stage filter allowing for zone overlap. A coordinate is accepted first when
-- -- it is inside any AcceptZone. Then RejectZones are checked, which enforces both borders, but also overlaps of
-- -- Accept- and RejectZones. Last, if it is inside a conflict zone, it is accepted.
--- `mybluemantis:AddZones(AcceptZones,RejectZones,ConflictZones)`
+-- mybluemantis:AddZones(AcceptZones,RejectZones,ConflictZones)
--
--
-- ### 2.1.2 Change the number of long-, mid- and short-range systems going live on a detected target:
--
-- -- parameters are numbers. Defaults are 1,2,2,6 respectively
--- `mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic)`
+-- mybluemantis:SetMaxActiveSAMs(Short,Mid,Long,Classic)
--
-- ### 2.1.3 SHORAD will automatically be added from SAM sites of type "short-range"
--
-- ### 2.1.4 Advanced features
--
-- -- switch off auto mode **before** you start MANTIS.
--- `mybluemantis.automode = false`
+-- mybluemantis.automode = false
--
-- -- switch off auto shorad **before** you start MANTIS.
--- `mybluemantis.autoshorad = false`
+-- mybluemantis.autoshorad = false
--
-- -- scale of the activation range, i.e. don't activate at the fringes of max range, defaults below.
-- -- also see engagerange below.
--- ` self.radiusscale[MANTIS.SamType.LONG] = 1.1`
--- ` self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2`
--- ` self.radiusscale[MANTIS.SamType.SHORT] = 1.3`
+-- self.radiusscale[MANTIS.SamType.LONG] = 1.1
+-- self.radiusscale[MANTIS.SamType.MEDIUM] = 1.2
+-- self.radiusscale[MANTIS.SamType.SHORT] = 1.3
+--
+-- ### 2.1.5 Friendlies check in firing range
+--
+-- -- For some scenarios, like Cold War, it might be useful not to activate SAMs if friendly aircraft are around to avoid death by friendly fire.
+-- mybluemantis.checkforfriendlies = true
--
-- # 3. Default settings [both modes unless stated otherwise]
--
@@ -321,6 +327,7 @@ MANTIS = {
automode = true,
autoshorad = true,
ShoradGroupSet = nil,
+ checkforfriendlies = false,
}
--- Advanced state enumerator
@@ -502,7 +509,8 @@ 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.
-
+
+ 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
@@ -631,7 +639,7 @@ do
-- TODO Version
-- @field #string version
- self.version="0.8.16"
+ self.version="0.8.18"
self:I(string.format("***** Starting MANTIS Version %s *****", self.version))
--- FSM Functions ---
@@ -1255,6 +1263,10 @@ do
-- DEBUG
set = self:_PreFilterHeight(height)
end
+ local friendlyset -- Core.Set#SET_GROUP
+ if self.checkforfriendlies == true then
+ friendlyset = SET_GROUP:New():FilterCoalitions(self.Coalition):FilterCategories({"plane","helicopter"}):FilterFunction(function(grp) if grp and grp:InAir() then return true else return false end end):FilterOnce()
+ end
for _,_coord in pairs (set) do
local coord = _coord -- get current coord to check
-- output for cross-check
@@ -1279,8 +1291,16 @@ do
local m = MESSAGE:New(text,10,"Check"):ToAllIf(self.debug)
self:T(self.lid..text)
end
+ -- friendlies around?
+ local nofriendlies = true
+ if self.checkforfriendlies == true then
+ local closestfriend, distance = friendlyset:GetClosestGroup(samcoordinate)
+ if closestfriend and distance and distance < rad then
+ nofriendlies = false
+ end
+ end
-- end output to cross-check
- if targetdistance <= rad and zonecheck then
+ if targetdistance <= rad and zonecheck == true and nofriendlies == true then
return true, targetdistance
end
end
diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua
index bcc11cfd1..3472132b5 100644
--- a/Moose Development/Moose/Functional/Range.lua
+++ b/Moose Development/Moose/Functional/Range.lua
@@ -7,7 +7,7 @@
-- Implementation is based on the [Simple Range Script](https://forums.eagle.ru/showthread.php?t=157991) by Ciribob, which itself was motivated
-- by a script by SNAFU [see here](https://forums.eagle.ru/showthread.php?t=109174).
--
--- [476th - Air Weapons Range Objects mod](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class.
+-- [476th - Air Weapons Range Objects mod](https://www.476vfightergroup.com/downloads.php?do=download&downloadid=482) is highly recommended for this class.
--
-- **Main Features:**
--
@@ -46,7 +46,7 @@
--
-- ### Contributions: FlightControl, Ciribob
-- ### SRS Additions: Applevangelist
---
+--
-- ===
-- @module Functional.Range
-- @image Range.JPG
@@ -102,7 +102,7 @@
-- @field #string targetpath Path where to save the target sheets.
-- @field #string targetprefix File prefix for target sheet files.
-- @field Sound.SRS#MSRS controlmsrs SRS wrapper for range controller.
--- @field Sound.SRS#MSRSQUEUE controlsrsQ SRS queue for range controller.
+-- @field Sound.SRS#MSRSQUEUE controlsrsQ SRS queue for range controller.
-- @field Sound.SRS#MSRS instructmsrs SRS wrapper for range instructor.
-- @field Sound.SRS#MSRSQUEUE instructsrsQ SRS queue for range instructor.
-- @field #number Coalition Coalition side for the menu, if any.
@@ -169,7 +169,7 @@
--
-- ## Specifying Coordinates
--
--- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified
+-- It is also possible to specify coordinates rather than unit or static objects as bombing target locations. This has the advantage, that even when the unit/static object is dead, the specified
-- coordinate will still be a valid impact point. This can be done via the @{#RANGE.AddBombingTargetCoordinate}(*coord*, *name*, *goodhitrange*) function.
--
-- # Fine Tuning
@@ -231,8 +231,8 @@
-- By default, the sound files are placed in the "Range Soundfiles/" folder inside the mission (.miz) file. Another folder can be specified via the @{#RANGE.SetSoundfilesPath}(*path*) function.
--
-- ## Voice output via SRS
---
--- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}().
+--
+-- Alternatively, the voice output can be fully done via SRS, **no sound file additions needed**. Set up SRS with @{#RANGE.SetSRS}().
-- Range control and instructor frequencies and voices can then be set via @{#RANGE.SetSRSRangeControl}() and @{#RANGE.SetSRSRangeInstructor}().
--
-- # Persistence
@@ -243,11 +243,11 @@
-- The next time you start the mission, these results are also automatically loaded.
--
-- Strafing results are currently **not** saved.
---
+--
-- # FSM Events
---
+--
-- This class creates additional events that can be used by mission designers for custom reactions
---
+--
-- * `EnterRange` when a player enters a range zone. See @{#RANGE.OnAfterEnterRange}
-- * `ExitRange` when a player leaves a range zone. See @{#RANGE.OnAfterExitRange}
-- * `Impact` on impact of a player's weapon on a bombing target. See @{#RANGE.OnAfterImpact}
@@ -371,7 +371,7 @@ RANGE = {
-- @param #number boxlength Length of strafe pit box in meters.
-- @param #number boxwidth Width of strafe pit box in meters.
-- @param #number goodpass Number of hits for a good strafing pit pass.
--- @param #number foulline Distance of foul line in meters.
+-- @param #number foulline Distance of foul line in meters.
RANGE.Defaults = {
goodhitrange = 25,
strafemaxalt = 914,
@@ -625,9 +625,9 @@ function RANGE:New( RangeName, Coalition )
-- Get range name.
-- TODO: make sure that the range name is not given twice. This would lead to problems in the F10 radio menu.
self.rangename = RangeName or "Practice Range"
-
+
self.Coalition = Coalition
-
+
-- Log id.
self.lid = string.format( "RANGE %s | ", self.rangename )
@@ -993,9 +993,9 @@ end
-- @param #string Host Host. Default "127.0.0.1".
-- @return #RANGE self
function RANGE:SetFunkManOn(Port, Host)
-
+
self.funkmanSocket=SOCKET:New(Port, Host)
-
+
return self
end
@@ -1200,7 +1200,7 @@ end
-- @param #string PathToSRS Path to SRS directory.
-- @param #number Port SRS port. Default 5002.
-- @param #number Coalition Coalition side, e.g. `coalition.side.BLUE` or `coalition.side.RED`. Default `coalition.side.BLUE`.
--- @param #number Frequency Frequency to use. Default is 256 MHz for range control and 305 MHz for instructor. If given, both control and instructor get this frequency.
+-- @param #number Frequency Frequency to use. Default is 256 MHz for range control and 305 MHz for instructor. If given, both control and instructor get this frequency.
-- @param #number Modulation Modulation to use, defaults to radio.modulation.AM
-- @param #number Volume Volume, between 0.0 and 1.0. Defaults to 1.0
-- @param #string PathToGoogleKey Path to Google TTS credentials.
@@ -1208,9 +1208,9 @@ end
function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume, PathToGoogleKey)
if PathToSRS or MSRS.path then
-
+
self.useSRS=true
-
+
self.controlmsrs=MSRS:New(PathToSRS or MSRS.path, Frequency or 256, Modulation or radio.modulation.AM)
self.controlmsrs:SetPort(Port or MSRS.port)
self.controlmsrs:SetCoalition(Coalition or coalition.side.BLUE)
@@ -1224,14 +1224,12 @@ function RANGE:SetSRS(PathToSRS, Port, Coalition, Frequency, Modulation, Volume,
self.instructmsrs:SetLabel("RANGEI")
self.instructmsrs:SetVolume(Volume or 1.0)
self.instructsrsQ = MSRSQUEUE:New("INSTRUCT")
-
- if PathToGoogleKey then
- self.controlmsrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey)
- self.controlmsrs:SetProvider(MSRS.Provider.GOOGLE)
- self.instructmsrs:SetProviderOptionsGoogle(PathToGoogleKey,PathToGoogleKey)
- self.instructmsrs:SetProvider(MSRS.Provider.GOOGLE)
+
+ if PathToGoogleKey then
+ self.controlmsrs:SetGoogle(PathToGoogleKey)
+ self.instructmsrs:SetGoogle(PathToGoogleKey)
end
-
+
else
self:E(self.lid..string.format("ERROR: No SRS path specified!"))
end
@@ -1741,9 +1739,9 @@ end
-- @param Core.Event#EVENTDATA EventData
function RANGE:OnEventBirth( EventData )
self:F( { eventbirth = EventData } )
-
+
if not EventData.IniPlayerName then return end
-
+
local _unitName = EventData.IniUnitName
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
@@ -1764,7 +1762,7 @@ function RANGE:OnEventBirth( EventData )
-- Reset current strafe status.
self.strafeStatus[_uid] = nil
-
+
if self.Coalition then
if EventData.IniCoalition == self.Coalition then
self:ScheduleOnce( 0.1, self._AddF10Commands, self, _unitName )
@@ -1773,7 +1771,7 @@ function RANGE:OnEventBirth( EventData )
-- Add Menu commands after a delay of 0.1 seconds.
self:ScheduleOnce( 0.1, self._AddF10Commands, self, _unitName )
end
-
+
-- By default, some bomb impact points and do not flare each hit on target.
self.PlayerSettings[_playername] = {} -- #RANGE.PlayerData
self.PlayerSettings[_playername].smokebombimpact = self.defaultsmokebomb
@@ -1907,21 +1905,21 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV
local _distance = nil
local _closeCoord = nil --Core.Point#COORDINATE
local _hitquality = "POOR"
-
+
-- Get callsign.
local _callsign = self:_myname( playerData.unitname )
-
+
local _playername=playerData.playername
-
+
local _unit=playerData.unit
-
+
-- Coordinate of impact point.
local impactcoord = weapon:GetImpactCoordinate()
-
+
-- Check if impact happened in range zone.
local insidezone = self.rangezone:IsCoordinateInZone( impactcoord )
-
+
-- Smoke impact point of bomb.
if playerData.smokebombimpact and insidezone then
if playerData.delaysmoke then
@@ -1930,19 +1928,19 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV
impactcoord:Smoke( playerData.smokecolor )
end
end
-
+
-- Loop over defined bombing targets.
for _, _bombtarget in pairs( self.bombingTargets ) do
local bombtarget=_bombtarget --#RANGE.BombTarget
-
+
-- Get target coordinate.
local targetcoord = self:_GetBombTargetCoordinate( _bombtarget )
-
+
if targetcoord then
-
+
-- Distance between bomb and target.
local _temp = impactcoord:Get2DDistance( targetcoord )
-
+
-- Find closest target to last known position of the bomb.
if _distance == nil or _temp < _distance then
_distance = _temp
@@ -1959,21 +1957,21 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV
else
_hitquality = "POOR"
end
-
+
end
end
end
-
+
-- Count if bomb fell less than ~1 km away from the target.
if _distance and _distance <= self.scorebombdistance then
-- Init bomb player results.
if not self.bombPlayerResults[_playername] then
self.bombPlayerResults[_playername] = {}
end
-
+
-- Local results.
local _results = self.bombPlayerResults[_playername]
-
+
local result = {} -- #RANGE.BombResult
result.command=SOCKET.DataType.BOMBRESULT
result.name = _closetTarget.name or "unknown"
@@ -1995,24 +1993,24 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV
result.attackVel = attackVel
result.attackAlt = attackAlt
result.date=os and os.date() or "n/a"
-
+
-- Add to table.
table.insert( _results, result )
-
+
-- Call impact.
self:Impact( result, playerData )
-
+
elseif insidezone then
-
+
-- Send message.
-- DONE SRS message
local _message = string.format( "%s, weapon impacted too far from nearest range target (>%.1f km). No score!", _callsign, self.scorebombdistance / 1000 )
if self.useSRS then
local ttstext = string.format( "%s, weapon impacted too far from nearest range target, mor than %.1f kilometer. No score!", _callsign, self.scorebombdistance / 1000 )
- self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2)
+ self.controlsrsQ:NewTransmission(ttstext,nil,self.controlmsrs,nil,2)
end
self:_DisplayMessageToGroup( _unit, _message, nil, false )
-
+
if self.rangecontrol then
-- weapon impacted too far from the nearest target! No Score!
if self.useSRS then
@@ -2021,11 +2019,11 @@ function RANGE._OnImpact(weapon, self, playerData, attackHdg, attackAlt, attackV
self.rangecontrol:NewTransmission( RANGE.Sound.RCWeaponImpactedTooFar.filename, RANGE.Sound.RCWeaponImpactedTooFar.duration, self.soundpath, nil, nil, _message, self.subduration )
end
end
-
+
else
self:T( self.lid .. "Weapon impacted outside range zone." )
end
-
+
end
--- Range event handler for event shot (when a unit releases a rocket or bomb (but not a fast firing gun).
@@ -2038,7 +2036,7 @@ function RANGE:OnEventShot( EventData )
if EventData.Weapon == nil or EventData.IniDCSUnit == nil or EventData.IniPlayerName == nil then
return
end
-
+
-- Create weapon object.
local weapon=WEAPON:New(EventData.weapon)
@@ -2050,7 +2048,7 @@ function RANGE:OnEventShot( EventData )
-- Get player unit and name.
local _unit, _playername = self:_GetPlayerUnitAndName( _unitName )
-
+
-- Distance Player-to-Range. Set this to larger value than the threshold.
local dPR = self.BombtrackThreshold * 2
@@ -2065,16 +2063,16 @@ function RANGE:OnEventShot( EventData )
-- Player data.
local playerData = self.PlayerSettings[_playername] -- #RANGE.PlayerData
-
+
-- Attack parameters.
local attackHdg=_unit:GetHeading()
local attackAlt=_unit:GetHeight()
attackAlt = UTILS.MetersToFeet(attackAlt)
- local attackVel=_unit:GetVelocityKNOTS()
+ local attackVel=_unit:GetVelocityKNOTS()
-- Tracking info and init of last bomb position.
self:T( self.lid .. string.format( "RANGE %s: Tracking %s - %s.", self.rangename, weapon:GetTypeName(), weapon:GetName()))
-
+
-- Set callback function on impact.
weapon:SetFuncImpact(RANGE._OnImpact, self, playerData, attackHdg, attackAlt, attackVel)
@@ -2146,33 +2144,33 @@ end
function RANGE:onafterEnterRange( From, Event, To, player )
if self.instructor and self.rangecontrol then
-
+
if self.useSRS then
-
-
+
+
local text = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f MHz", self.rangecontrolfreq)
local ttstext = string.format("You entered the bombing range. For hit assessment, contact the range controller at %.3f mega hertz.", self.rangecontrolfreq)
-
+
local group = player.client:GetGroup()
-
+
self.instructsrsQ:NewTransmission(ttstext, nil, self.instructmsrs, nil, 1, {group}, text, 10)
-
+
else
-
+
-- Range control radio frequency split.
local RF = UTILS.Split( string.format( "%.3f", self.rangecontrolfreq ), "." )
-
+
-- Radio message that player entered the range
-
+
-- You entered the bombing range. For hit assessment, contact the range controller at xy MHz
self.instructor:NewTransmission( RANGE.Sound.IREnterRange.filename, RANGE.Sound.IREnterRange.duration, self.soundpath )
self.instructor:Number2Transmission( RF[1] )
-
+
if tonumber( RF[2] ) > 0 then
self.instructor:NewTransmission( RANGE.Sound.IRDecimal.filename, RANGE.Sound.IRDecimal.duration, self.soundpath )
self.instructor:Number2Transmission( RF[2] )
end
-
+
self.instructor:NewTransmission( RANGE.Sound.IRMegaHertz.filename, RANGE.Sound.IRMegaHertz.duration, self.soundpath )
end
end
@@ -2190,11 +2188,11 @@ function RANGE:onafterExitRange( From, Event, To, player )
if self.instructor then
-- You left the bombing range zone. Have a nice day!
if self.useSRS then
-
+
local text = "You left the bombing range zone. "
-
+
local r=math.random(5)
-
+
if r==1 then
text=text.."Have a nice day!"
elseif r==2 then
@@ -2204,9 +2202,9 @@ function RANGE:onafterExitRange( From, Event, To, player )
elseif r==4 then
text=text.."See you in two weeks!"
elseif r==5 then
- text=text.."!"
+ text=text.."!"
end
-
+
self.instructsrsQ:NewTransmission(text, nil, self.instructmsrs, nil, 1, {player.client:GetGroup()}, text, 10)
else
self.instructor:NewTransmission( RANGE.Sound.IRExitRange.filename, RANGE.Sound.IRExitRange.duration, self.soundpath )
@@ -2240,7 +2238,7 @@ function RANGE:onafterImpact( From, Event, To, result, player )
text = text .. string.format( " %s hit.", result.quality )
if self.rangecontrol then
-
+
if self.useSRS then
local group = player.client:GetGroup()
self.controlsrsQ:NewTransmission(text,nil,self.controlmsrs,nil,1,{group},text,10)
@@ -2265,10 +2263,10 @@ function RANGE:onafterImpact( From, Event, To, result, player )
-- Unit.
if player.unitname and not self.useSRS then
-
+
-- Get unit.
local unit = UNIT:FindByName( player.unitname )
-
+
-- Send message.
self:_DisplayMessageToGroup( unit, text, nil, true )
self:T( self.lid .. text )
@@ -2278,7 +2276,7 @@ function RANGE:onafterImpact( From, Event, To, result, player )
if self.autosave then
self:Save()
end
-
+
-- Send result to FunkMan, which creates fancy MatLab figures and sends them to Discord via a bot.
if self.funkmanSocket then
self.funkmanSocket:SendTable(result)
@@ -2547,7 +2545,7 @@ function RANGE:_DisplayMyStrafePitResults( _unitName )
local _message = string.format( "My Top %d Strafe Pit Results:\n", self.ndisplayresult )
-- Get player results.
- local _results = self.strafePlayerResults[_playername]
+ local _results = self.strafePlayerResults[_playername]
-- Create message.
if _results == nil then
@@ -2853,7 +2851,7 @@ function RANGE:_DisplayRangeInfo( _unitname )
end
end
text = text .. string.format( "Instructor %.3f MHz (Relay=%s)\n", self.instructorfreq, alive )
- end
+ end
if self.rangecontrol then
local alive = "N/A"
if self.rangecontrolrelayname then
@@ -3081,10 +3079,10 @@ function RANGE:_CheckInZone( _unitName )
local unitheading = 0 -- RangeBoss
if _unit and _playername then
-
+
-- Player data.
local playerData=self.PlayerSettings[_playername] -- #RANGE.PlayerData
-
+
--- Function to check if unit is in zone and facing in the right direction and is below the max alt.
local function checkme( targetheading, _zone )
local zone = _zone -- Core.Zone#ZONE
@@ -3098,7 +3096,7 @@ function RANGE:_CheckInZone( _unitName )
if towardspit then
local vec3 = _unit:GetVec3()
- local vec2 = { x = vec3.x, y = vec3.z } -- DCS#Vec2
+ local vec2 = { x = vec3.x, y = vec3.z } -- DCS#Vec2
local landheight = land.getHeight( vec2 )
local unitalt = vec3.y - landheight
@@ -3145,7 +3143,7 @@ function RANGE:_CheckInZone( _unitName )
-- Send message.
self:_DisplayMessageToGroup( _unit, _msg, nil, true )
-
+
if self.rangecontrol then
if self.useSRS then
local group = _unit:GetGroup()
@@ -3164,9 +3162,9 @@ function RANGE:_CheckInZone( _unitName )
-- Result.
local _result = self.strafeStatus[_unitID] --#RANGE.StrafeStatus
-
+
local _sound = nil -- #RANGE.Soundfile
-
+
-- Calculate accuracy of run. Number of hits wrt number of rounds fired.
local shots = _result.ammo - _ammo
local accur = 0
@@ -3176,7 +3174,7 @@ function RANGE:_CheckInZone( _unitName )
accur = 100
end
end
-
+
-- Results text and sound message.
local resulttext=""
if _result.pastfoulline == true then --
@@ -3213,7 +3211,7 @@ function RANGE:_CheckInZone( _unitName )
-- Send message.
self:_DisplayMessageToGroup( _unit, _text )
-
+
-- Strafe result.
local result = {} -- #RANGE.StrafeResult
result.command=SOCKET.DataType.STRAFERESULT
@@ -3230,14 +3228,14 @@ function RANGE:_CheckInZone( _unitName )
result.rangename = self.rangename
result.airframe=playerData.airframe
result.invalid = _result.pastfoulline
-
+
-- Griger Results.
self:StrafeResult(playerData, result)
-
+
-- Save trap sheet.
if playerData and playerData.targeton and self.targetsheet then
self:_SaveTargetSheet( _playername, result )
- end
+ end
-- Voice over.
if self.rangecontrol then
@@ -3302,7 +3300,7 @@ function RANGE:_CheckInZone( _unitName )
-- Send message.
self:_DisplayMessageToGroup( _unit, _msg, 10, true )
-
+
-- Trigger event that player is rolling in.
self:RollingIn(playerData, target)
@@ -3438,18 +3436,18 @@ function RANGE:_GetBombTargetCoordinate( target )
local coord = nil -- Core.Point#COORDINATE
if target.type == RANGE.TargetType.UNIT then
-
+
-- Check if alive
if target.target and target.target:IsAlive() then
-- Get current position.
coord = target.target:GetCoordinate()
-- Save as last known position in case target dies.
- target.coordinate=coord
+ target.coordinate=coord
else
-- Use stored position.
coord = target.coordinate
end
-
+
elseif target.type == RANGE.TargetType.STATIC then
-- Static targets dont move.
@@ -3459,11 +3457,11 @@ function RANGE:_GetBombTargetCoordinate( target )
-- Coordinates dont move.
coord = target.coordinate
-
+
elseif target.type == RANGE.TargetType.SCENERY then
-- Coordinates dont move.
- coord = target.coordinate
+ coord = target.coordinate
else
self:E( self.lid .. "ERROR: Unknown target type." )
@@ -3670,7 +3668,7 @@ function RANGE:_DisplayMessageToGroup( _unit, _text, _time, _clear, display, _to
local playermessage = self.PlayerSettings[playername].messages
-- Send message to player if messages enabled and not only for the examiner.
-
+
if _gid and (playermessage == true or display) and (not self.examinerexclusive) then
if _togroup and _grp then
local m = MESSAGE:New(_text,_time,nil,_clear):ToGroup(_grp)
@@ -4025,9 +4023,9 @@ function RANGE:_GetPlayerUnitAndName( _unitName )
self:F2( _unitName )
if _unitName ~= nil then
-
+
local multiplayer = false
-
+
-- Get DCS unit from its name.
local DCSunit = Unit.getByName( _unitName )
@@ -4066,7 +4064,7 @@ function RANGE:_myname( unitname )
if grp and grp:IsAlive() then
pname = grp:GetCustomCallSign(true,true)
end
- end
+ end
return pname
end
diff --git a/Moose Development/Moose/Functional/Stratego.lua b/Moose Development/Moose/Functional/Stratego.lua
index 07bac3837..adb440470 100644
--- a/Moose Development/Moose/Functional/Stratego.lua
+++ b/Moose Development/Moose/Functional/Stratego.lua
@@ -15,7 +15,7 @@
--
-- @module Functional.Stratego
-- @image Functional.Stratego.png
--- Last Update April 2024
+-- Last Update May 2024
---
@@ -42,6 +42,7 @@
-- @field #boolean usebudget
-- @field #number CaptureUnits
-- @field #number CaptureThreatlevel
+-- @field #table CaptureObjectCategories
-- @field #boolean ExcludeShips
-- @field Core.Zone#ZONE StrategoZone
-- @extends Core.Base#BASE
@@ -180,7 +181,7 @@ STRATEGO = {
debug = false,
drawzone = false,
markzone = false,
- version = "0.2.7",
+ version = "0.3.1",
portweight = 3,
POIweight = 1,
maxrunways = 3,
@@ -199,6 +200,7 @@ STRATEGO = {
usebudget = false,
CaptureUnits = 3,
CaptureThreatlevel = 1,
+ CaptureObjectCategories = {Object.Category.UNIT},
ExcludeShips = true,
}
@@ -210,9 +212,10 @@ STRATEGO = {
-- @field #number coalition
-- @field #boolean port
-- @field Core.Zone#ZONE_RADIUS zone,
--- @field Core.Point#COORDINATRE coord
+-- @field Core.Point#COORDINATE coord
-- @field #string type
-- @field Ops.OpsZone#OPSZONE opszone
+-- @field #number connections
---
-- @type STRATEGO.DistData
@@ -419,11 +422,13 @@ end
-- @param #STRATEGO self
-- @param #number CaptureUnits Number of units needed, defaults to three.
-- @param #number CaptureThreatlevel Threat level needed, can be 0..10, defaults to one.
+-- @param #table CaptureCategories Table of object categories which can capture a node, defaults to `{Object.Category.UNIT}`.
-- @return #STRATEGO self
-function STRATEGO:SetCaptureOptions(CaptureUnits,CaptureThreatlevel)
+function STRATEGO:SetCaptureOptions(CaptureUnits,CaptureThreatlevel,CaptureCategories)
self:T(self.lid.."SetCaptureOptions")
self.CaptureUnits = CaptureUnits or 3
self.CaptureThreatlevel = CaptureThreatlevel or 1
+ self.CaptureObjectCategories = CaptureCategories or {Object.Category.UNIT}
return self
end
@@ -486,6 +491,7 @@ function STRATEGO:AnalyseBases()
coord = coord,
type = abtype,
opszone = opszone,
+ connections = 0,
}
airbasetable[abname] = tbl
nonconnectedab[abname] = true
@@ -526,6 +532,7 @@ function STRATEGO:GetNewOpsZone(Zone,Coalition)
local opszone = OPSZONE:New(Zone,Coalition or 0)
opszone:SetCaptureNunits(self.CaptureUnits)
opszone:SetCaptureThreatlevel(self.CaptureThreatlevel)
+ opszone:SetObjectCategories(self.CaptureObjectCategories)
opszone:SetDrawZone(self.drawzone)
opszone:SetMarkZone(self.markzone)
opszone:Start()
@@ -571,10 +578,12 @@ function STRATEGO:AnalysePOIs(Set,Weight,Key)
coord = coord,
type = Key,
opszone = opszone,
+ connections = 0,
}
- airbasetable[zone:GetName()] = tbl
- nonconnectedab[zone:GetName()] = true
+ airbasetable[zname] = tbl
+ nonconnectedab[zname] = true
local name = string.gsub(zname,"[%p%s]",".")
+ --self:I({name=name,zone=zname})
easynames[name]=zname
end
)
@@ -585,7 +594,7 @@ end
-- @param #STRATEGO self
-- @return #STRATEGO self
function STRATEGO:GetToFrom(StartPoint,EndPoint)
- self:T(self.lid.."GetToFrom")
+ self:T(self.lid.."GetToFrom "..tostring(StartPoint).." "..tostring(EndPoint))
local pstart = string.gsub(StartPoint,"[%p%s]",".")
local pend = string.gsub(EndPoint,"[%p%s]",".")
local fromto = pstart..";"..pend
@@ -593,11 +602,35 @@ function STRATEGO:GetToFrom(StartPoint,EndPoint)
return fromto, tofrom
end
+--- [USER] Get available connecting nodes from one start node
+-- @param #STRATEGO self
+-- @param #string StartPoint The starting name
+-- @return #boolean found
+-- @return #table Nodes
+function STRATEGO:GetRoutesFromNode(StartPoint)
+ self:T(self.lid.."GetRoutesFromNode")
+ local pstart = string.gsub(StartPoint,"[%p%s]",".")
+ local found = false
+ pstart=pstart..";"
+ local routes = {}
+ local listed = {}
+ for _,_data in pairs(self.routexists) do
+ if string.find(_data,pstart,1,true) and not listed[_data] then
+ local target = string.gsub(_data,pstart,"")
+ local fname = self.easynames[target]
+ table.insert(routes,fname)
+ found = true
+ listed[_data] = true
+ end
+ end
+ return found,routes
+end
+
--- [USER] Manually add a route, for e.g. Island hopping or to connect isolated networks. Use **after** STRATEGO has been started!
-- @param #STRATEGO self
-- @param #string Startpoint Starting Point, e.g. AIRBASE.Syria.Hatay
-- @param #string Endpoint End Point, e.g. AIRBASE.Syria.H4
--- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to lila.
+-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to violet.
-- @param #number Linetype (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 5.
-- @param #boolean Draw (Optional) If true, draw route on the F10 map. Defaukt false.
-- @return #STRATEGO self
@@ -625,6 +658,8 @@ function STRATEGO:AddRoutesManually(Startpoint,Endpoint,Color,Linetype,Draw)
local factor = self.airbasetable[Startpoint].baseweight*self.routefactor
self.airbasetable[Startpoint].weight = self.airbasetable[Startpoint].weight+factor
self.airbasetable[Endpoint].weight = self.airbasetable[Endpoint].weight+factor
+ self.airbasetable[Endpoint].connections = self.airbasetable[Endpoint].connections + 2
+ self.airbasetable[Startpoint].connections = self.airbasetable[Startpoint].connections+2
if self.debug or Draw then
startcoordinate:LineToAll(targetcoordinate,-1,color,1,linetype,nil,string.format("%dkm",dist))
end
@@ -643,7 +678,7 @@ function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype)
for _,_data in pairs(self.airbasetable) do
local fromto,tofrom = self:GetToFrom(startpoint,_data.name)
if _data.name == startpoint then
- -- sam as we
+ -- same as we
elseif _data.baseweight == tgtrwys and not (self.routexists[fromto] or self.routexists[tofrom]) then
local tgtc = _data.coord
local dist = UTILS.Round(tgtc:Get2DDistance(startcoord),-2)/1000
@@ -665,6 +700,8 @@ function STRATEGO:AnalyseRoutes(tgtrwys,factor,color,linetype)
self.nonconnectedab[startpoint] = false
self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+factor
self.airbasetable[_data.name].weight = self.airbasetable[_data.name].weight+factor
+ self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections + 1
+ self.airbasetable[_data.name].connections = self.airbasetable[_data.name].connections + 1
if self.debug then
startcoord:LineToAll(tgtc,-1,color,1,linetype,nil,string.format("%dkm",dist))
end
@@ -711,6 +748,8 @@ function STRATEGO:AnalyseUnconnected(Color)
end
self.airbasetable[startpoint].weight = self.airbasetable[startpoint].weight+1
self.airbasetable[closest].weight = self.airbasetable[closest].weight+1
+ self.airbasetable[startpoint].connections = self.airbasetable[startpoint].connections+2
+ self.airbasetable[closest].connections = self.airbasetable[closest].connections+2
local data = {
start = startpoint,
target = closest,
@@ -727,14 +766,50 @@ function STRATEGO:AnalyseUnconnected(Color)
return self
end
+--[[
+function STRATEGO:PruneDeadEnds(abtable)
+ local found = false
+ local newtable = {}
+ for name, _data in pairs(abtable) do
+ local data = _data -- #STRATEGO.Data
+ if data.connections > 2 then
+ newtable[name] = data
+ else
+ -- dead end
+ found = true
+ local neighbors, nearest, distance = self:FindNeighborNodes(name)
+ --self:I("Pruning "..name)
+ if nearest then
+ for _name,_ in pairs(neighbors) do
+ local abname = self.easynames[_name] or _name
+ --self:I({easyname=_name,airbasename=abname})
+ if abtable[abname] then
+ abtable[abname].connections = abtable[abname].connections -1
+ end
+ end
+ end
+ if self.debug then
+ data.coord:CircleToAll(5000,-1,{1,1,1},1,{1,1,1},1,3,true,"Dead End")
+ end
+ end
+ end
+ abtable = nil
+ return found,newtable
+end
+--]]
+
--- [USER] Get a list of the nodes with the highest weight.
-- @param #STRATEGO self
-- @param #number Coalition (Optional) Find for this coalition only. E.g. coalition.side.BLUE.
-- @return #table Table of nodes.
-- @return #number Weight The consolidated weight associated with the nodes.
+-- @return #number Highest Highest weight found.
+-- @return #string Name of the node with the highest weight.
function STRATEGO:GetHighestWeightNodes(Coalition)
self:T(self.lid.."GetHighestWeightNodes")
local weight = 0
+ local highest = 0
+ local highname = nil
local airbases = {}
for _name,_data in pairs(self.airbasetable) do
local okay = true
@@ -748,8 +823,12 @@ function STRATEGO:GetHighestWeightNodes(Coalition)
if not airbases[weight] then airbases[weight]={} end
table.insert(airbases[weight],_name)
end
+ if _data.weight > highest and okay then
+ highest = _data.weight
+ highname = _name
+ end
end
- return airbases[weight],weight
+ return airbases[weight],weight,highest,highname
end
--- [USER] Get a list of the nodes a weight less than the given parameter.
@@ -1136,35 +1215,67 @@ function STRATEGO:FindNeighborNodes(Name,Enemies,Friends)
self:T(self.lid.."FindNeighborNodes")
local neighbors = {}
local name = string.gsub(Name,"[%p%s]",".")
+ --self:I({Name=Name,name=name})
local shortestdist = 1000*1000
local nearest = nil
for _route,_data in pairs(self.disttable) do
if string.find(_route,name,1,true) then
local dist = self.disttable[_route] -- #STRATEGO.DistData
+ --self:I({route=_route,name=name})
local tname = string.gsub(_route,name,"")
local tname = string.gsub(tname,";","")
+ --self:I({tname=tname,cname=self.easynames[tname]})
local cname = self.easynames[tname] -- name of target
- local encoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
- if Enemies == true then
- if self.airbasetable[cname].coalition == encoa then
- neighbors[cname] = dist
+ if cname then
+ local encoa = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
+ if Enemies == true then
+ if self.airbasetable[cname].coalition == encoa then
+ neighbors[cname] = dist
+ end
+ elseif Friends == true then
+ if self.airbasetable[cname].coalition ~= encoa then
+ neighbors[cname] = dist
+ end
+ else
+ neighbors[cname] = dist
end
- elseif Friends == true then
- if self.airbasetable[cname].coalition ~= encoa then
- neighbors[cname] = dist
+ if neighbors[cname] and dist.dist < shortestdist then
+ shortestdist = dist.dist
+ nearest = cname
end
- else
- neighbors[cname] = dist
- end
- if neighbors[cname] and dist.dist < shortestdist then
- shortestdist = dist.dist
- nearest = cname
end
end
end
return neighbors, nearest, shortestdist
end
+--- [INTERNAL] Route Finding - Find the next hop towards an end node from a start node
+-- @param #STRATEGO self
+-- @param #string Start The name of the start node.
+-- @param #string End The name of the end node.
+-- @param #table InRoute Table of node names making up the route so far.
+-- @return #string Name of the next closest node
+function STRATEGO:_GetNextClosest(Start,End,InRoute)
+ local ecoord = self.airbasetable[End].coord
+ local nodes,nearest = self:FindNeighborNodes(Start)
+ --self:I(tostring(nearest))
+ local closest = nil
+ local closedist = 1000*1000
+ for _name,_dist in pairs(nodes) do
+ local kcoord = self.airbasetable[_name].coord
+ local nnodes = self.airbasetable[_name].connections > 2 and true or false
+ if _name == End then nnodes = true end
+ if kcoord ~= nil and ecoord ~= nil and nnodes == true and InRoute[_name] ~= true then
+ local dist = math.floor((kcoord:Get2DDistance(ecoord)/1000)+0.5)
+ if (dist < closedist ) then
+ closedist = dist
+ closest = _name
+ end
+ end
+ end
+ return closest
+end
+
--- [USER] Find a route between two nodes.
-- @param #STRATEGO self
-- @param #string Start The name of the start node.
@@ -1173,15 +1284,19 @@ end
-- @param #boolean Draw If true, draw the route on the map.
-- @param #table Color (Optional) RGB color table {r, g, b}, e.g. {1,0,0} for red. Defaults to black.
-- @param #number LineType (Optional) Line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 6.
+-- @param #boolean NoOptimize If set to true, do not optimize (shorten) the resulting route if possible.
-- @return #table Route Table of #string name entries of the route
-- @return #boolean Complete If true, the route was found end-to-end.
-function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType)
+-- @return #boolean Reverse If true, the route was found with a reverse search, the route table will be from sorted from end point to start point.
+function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType,NoOptimize)
self:T(self.lid.."FindRoute")
--self:I({Start,End,Hops})
--local bases = UTILS.DeepCopy(self.airbasetable)
- local Route = {}
+ local Route = {}
+ local InRoute = {}
local hops = Hops or 4
local routecomplete = false
+ local reverse = false
local function Checker(neighbors)
for _name,_data in pairs(neighbors) do
@@ -1192,26 +1307,7 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType)
end
return nil
end
-
- local function NextClosest(Start,End)
- local ecoord = self.airbasetable[End].coord
- local nodes = self:FindNeighborNodes(Start)
- local closest = nil
- local closedist = 1000*1000
- for _name,_dist in pairs(nodes) do
- local kcoord = self.airbasetable[_name].coord
- local dist = math.floor((kcoord:Get2DDistance(ecoord)/1000)+0.5)
- if dist < closedist then
- closedist = dist
- closest = _name
- end
- end
- if closest then
- --MESSAGE:New(string.format("Start %s | End %s | Nextclosest %s",Start,End,closest),10,"STRATEGO"):ToLog():ToAll()
- return closest
- end
- end
-
+
local function DrawRoute(Route)
for i=1,#Route-1 do
local p1=Route[i]
@@ -1226,6 +1322,7 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType)
-- One hop
Route[#Route+1] = Start
+ InRoute[Start] = true
local nodes = self:FindNeighborNodes(Start)
local endpoint = Checker(nodes)
@@ -1235,9 +1332,11 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType)
else
local spoint = Start
for i=1,hops do
- local Next = NextClosest(spoint,End)
- if Next then
+ --self:I("Start="..tostring(spoint))
+ local Next = self:_GetNextClosest(spoint,End,InRoute)
+ if Next ~= nil then
Route[#Route+1] = Next
+ InRoute[Next] = true
local nodes = self:FindNeighborNodes(Next)
local endpoint = Checker(nodes)
if endpoint then
@@ -1247,11 +1346,59 @@ function STRATEGO:FindRoute(Start,End,Hops,Draw,Color,LineType)
else
spoint = Next
end
- end
+ else
+ break
+ end
end
end
- if (self.debug or Draw) then DrawRoute(Route) end
- return Route, routecomplete
+
+ -- optimize route
+ local function OptimizeRoute(Route)
+ local foundcut = false
+ local largestcut = 0
+ local cut = {}
+ for i=1,#Route do
+ --self:I({Start=Route[i]})
+ local found,nodes = self:GetRoutesFromNode(Route[i])
+ for _,_name in pairs(nodes or {}) do
+ for j=i+2,#Route do
+ if _name == Route[j] then
+ --self:I({"Shortcut",Route[i],Route[j]})
+ if j-i > largestcut then
+ largestcut = j-i
+ cut = {i=i,j=j}
+ foundcut = true
+ end
+ end
+ end
+ end
+ end
+ if foundcut then
+ local newroute = {}
+ for i=1,#Route do
+ if i<= cut.i or i>=cut.j then
+ table.insert(newroute,Route[i])
+ end
+ end
+ return newroute
+ end
+ return Route, foundcut
+ end
+
+ if routecomplete == true and NoOptimize ~= true then
+ local foundcut = true
+ while foundcut ~= false do
+ Route, foundcut = OptimizeRoute(Route)
+ end
+ else
+ -- reverse search
+ Route, routecomplete = self:FindRoute(End,Start,Hops,Draw,Color,LineType)
+ reverse = true
+ end
+
+ if (self.debug or Draw) then DrawRoute(Route) end
+
+ return Route, routecomplete, reverse
end
--- [USER] Add budget points.
@@ -1358,6 +1505,139 @@ function STRATEGO:FindAffordableConsolidationTarget()
end
end
+--- [INTERNAL] Internal helper function to check for islands, aka Floodtest
+-- @param #STRATEGO self
+-- @param #string next Name of the start node
+-- @param #table filled #table of visited nodes
+-- @param #table unfilled #table if unvisited nodes
+-- @return #STRATEGO self
+function STRATEGO:_FloodNext(next,filled,unfilled)
+ local start = self:FindNeighborNodes(next)
+ for _name,_ in pairs (start) do
+ if filled[_name] ~= true then
+ self:T("Flooding ".._name)
+ filled[_name] = true
+ unfilled[_name] = nil
+ self:_FloodNext(_name,filled,unfilled)
+ end
+ end
+ return self
+end
+
+--- [INTERNAL] Internal helper function to check for islands, aka Floodtest
+-- @param #STRATEGO self
+-- @param #string Start Name of the start node
+-- @param #table ABTable (Optional) #table of node names to check.
+-- @return #STRATEGO self
+function STRATEGO:_FloodFill(Start,ABTable)
+ self:T("Start = "..tostring(Start))
+ if Start == nil then return end
+ local filled = {}
+ local unfilled = {}
+ if ABTable then
+ unfilled = ABTable
+ else
+ for _name,_ in pairs(self.airbasetable) do
+ unfilled[_name] = true
+ end
+ end
+ filled[Start] = true
+ unfilled[Start] = nil
+ local start = self:FindNeighborNodes(Start)
+ for _name,_ in pairs (start) do
+ if filled[_name] ~= true then
+ self:T("Flooding ".._name)
+ filled[_name] = true
+ unfilled[_name] = nil
+ self:_FloodNext(_name,filled,unfilled)
+ end
+ end
+ return filled, unfilled
+end
+
+--- [INTERNAL] Internal helper function to check for islands, aka Floodtest
+-- @param #STRATEGO self
+-- @param #boolen connect If true, connect the two resulting islands at the shortest distance if necessary
+-- @param #boolen draw If true, draw outer vertices of found node networks
+-- @return #boolean Connected If true, all nodes are in one network
+-- @return #table Network #table of node names in the network
+-- @return #table Unconnected #table of node names **not** in the network
+function STRATEGO:_FloodTest(connect,draw)
+
+ local function GetElastic(bases)
+ local vec2table = {}
+ for _name,_ in pairs(bases) do
+ local coord = self.airbasetable[_name].coord
+ local vec2 = coord:GetVec2()
+ table.insert(vec2table,vec2)
+ end
+ local zone = ZONE_ELASTIC:New("STRATEGO-Floodtest-"..math.random(1,10000),vec2table)
+ return zone
+ end
+
+ local function DrawElastic(filled,drawit)
+ local zone = GetElastic(filled)
+ if drawit then
+ zone:SetColor({1,1,1},1)
+ zone:SetDrawCoalition(-1)
+ zone:Update(1,true) -- draw zone
+ end
+ return zone
+ end
+
+ local _,_,weight,name = self:GetHighestWeightNodes()
+ local filled, unfilled = self:_FloodFill(name)
+ local allin = true
+ if table.length(unfilled) > 0 then
+ MESSAGE:New("There is at least one node island!",15,"STRATEGO"):ToAllIf(self.debug):ToLog()
+ allin = false
+ if self.debug == true then
+ local zone1 = DrawElastic(filled,draw)
+ local zone2 = DrawElastic(unfilled,draw)
+ local vertices1 = zone1:GetVerticiesVec2()
+ local vertices2 = zone2:GetVerticiesVec2()
+ -- get closest vertices
+ local corner1 = nil
+ local corner2 = nil
+ local mindist = math.huge
+ local found = false
+ for _,_edge in pairs(vertices1) do
+ for _,_edge2 in pairs(vertices2) do
+ local dist=UTILS.VecDist2D(_edge,_edge2)
+ if dist < mindist then
+ mindist = dist
+ corner1 = _edge
+ corner2 = _edge2
+ found = true
+ end
+ end
+ end
+ if found then
+ local Corner = COORDINATE:NewFromVec2(corner1)
+ local Corner2 = COORDINATE:NewFromVec2(corner2)
+ Corner:LineToAll(Corner2,-1,{1,1,1},1,1,true,"Island2Island")
+ local cornername
+ local cornername2
+ for _name,_data in pairs(self.airbasetable) do
+ local zone = _data.zone
+ if zone:IsVec2InZone(corner1) then
+ cornername = _name
+ self:T("Corner1 = ".._name)
+ end
+ if zone:IsVec2InZone(corner2) then
+ cornername2 = _name
+ self:T("Corner2 = ".._name)
+ end
+ if cornername and cornername2 and connect == true then
+ self:AddRoutesManually(cornername,cornername2,Color,Linetype,self.debug)
+ end
+ end
+ end
+ end
+ end
+ return allin, filled, unfilled
+end
+
---------------------------------------------------------------------------------------------------------------
--
-- End
diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua
index 745bf817c..244e252ed 100644
--- a/Moose Development/Moose/Ops/Airboss.lua
+++ b/Moose Development/Moose/Ops/Airboss.lua
@@ -255,6 +255,7 @@
-- @field #boolean skipperUturn U-turn on/off via menu.
-- @field #number skipperOffset Holding offset angle in degrees for Case II/III manual recoveries.
-- @field #number skipperTime Recovery time in min for manual recovery.
+-- @field #boolean intowindold If true, use old into wind calculation.
-- @extends Core.Fsm#FSM
--- Be the boss!
@@ -2724,6 +2725,18 @@ function AIRBOSS:SetLSOCallInterval( TimeInterval )
return self
end
+--- Set if old into wind calculation is used when carrier turns into the wind for a recovery.
+-- @param #AIRBOSS self
+-- @param #boolean SwitchOn If `true` or `nil`, use old into wind calculation.
+-- @return #AIRBOSS self
+function AIRBOSS:SetIntoWindLegacy( SwitchOn )
+ if SwitchOn==nil then
+ SwitchOn=true
+ end
+ self.intowindold=SwitchOn
+ return self
+end
+
--- Airboss is a rather nice guy and not strictly following the rules. Fore example, he does allow you into the landing pattern if you are not coming from the Marshal stack.
-- @param #AIRBOSS self
-- @param #boolean Switch If true or nil, Airboss bends the rules a bit.
@@ -3642,6 +3655,12 @@ function AIRBOSS:onafterStatus( From, Event, To )
local pos = self:GetCoordinate()
local speed = self.carrier:GetVelocityKNOTS()
+ -- Update magnetic variation if we can get it from DCS.
+ if require then
+ self.magvar=pos:GetMagneticDeclination()
+ --env.info(string.format("FF magvar=%.1f", self.magvar))
+ end
+
-- Check water is ahead.
local collision = false -- self:_CheckCollisionCoord(pos:Translate(self.collisiondist, hdg))
@@ -5201,6 +5220,7 @@ function AIRBOSS:_InitVoiceOvers()
TOMCAT = { file = "PILOT-Tomcat", suffix = "ogg", loud = false, subtitle = "", duration = 0.66, subduration = 5 },
HORNET = { file = "PILOT-Hornet", suffix = "ogg", loud = false, subtitle = "", duration = 0.56, subduration = 5 },
VIKING = { file = "PILOT-Viking", suffix = "ogg", loud = false, subtitle = "", duration = 0.61, subduration = 5 },
+ GREYHOUND = { file = "PILOT-Greyhound", suffix = "ogg", loud = false, subtitle = "", duration = 0.61, subduration = 5 },
BALL = { file = "PILOT-Ball", suffix = "ogg", loud = false, subtitle = "", duration = 0.50, subduration = 5 },
BINGOFUEL = { file = "PILOT-BingoFuel", suffix = "ogg", loud = false, subtitle = "", duration = 0.80 },
GASATDIVERT = { file = "PILOT-GasAtDivert", suffix = "ogg", loud = false, subtitle = "", duration = 1.80 },
@@ -6475,7 +6495,7 @@ function AIRBOSS:_LandAI( flight )
or flight.actype == AIRBOSS.AircraftCarrier.RHINOF
or flight.actype == AIRBOSS.AircraftCarrier.GROWLER then
Speed = UTILS.KnotsToKmph( 200 )
- elseif flight.actype == AIRBOSS.AircraftCarrier.E2D then
+ elseif flight.actype == AIRBOSS.AircraftCarrier.E2D or flight.actype == AIRBOSS.AircraftCarrier.C2A then
Speed = UTILS.KnotsToKmph( 150 )
elseif flight.actype == AIRBOSS.AircraftCarrier.F14A_AI or flight.actype == AIRBOSS.AircraftCarrier.F14A or flight.actype == AIRBOSS.AircraftCarrier.F14B then
Speed = UTILS.KnotsToKmph( 175 )
@@ -11476,7 +11496,7 @@ end
--- Get wind direction and speed at carrier position.
-- @param #AIRBOSS self
--- @param #number alt Altitude ASL in meters. Default 15 m.
+-- @param #number alt Altitude ASL in meters. Default 18 m.
-- @param #boolean magnetic Direction including magnetic declination.
-- @param Core.Point#COORDINATE coord (Optional) Coordinate at which to get the wind. Default is current carrier position.
-- @return #number Direction the wind is blowing **from** in degrees.
@@ -11548,10 +11568,31 @@ end
--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway.
-- @param #AIRBOSS self
+-- @param #number vdeck Desired wind velocity over deck in knots.
-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned.
-- @param Core.Point#COORDINATE coord (Optional) Coordinate from which heading is calculated. Default is current carrier position.
-- @return #number Carrier heading in degrees.
-function AIRBOSS:GetHeadingIntoWind_old( magnetic, coord )
+-- @return #number Carrier speed in knots to reach desired wind speed on deck.
+function AIRBOSS:GetHeadingIntoWind(vdeck, magnetic, coord )
+
+ if self.intowindold then
+ --env.info("FF use OLD into wind")
+ return self:GetHeadingIntoWind_old(vdeck, magnetic, coord)
+ else
+ --env.info("FF use NEW into wind")
+ return self:GetHeadingIntoWind_new(vdeck, magnetic, coord)
+ end
+
+end
+
+
+--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway.
+-- @param #AIRBOSS self
+-- @param #number vdeck Desired wind velocity over deck in knots.
+-- @param #boolean magnetic If true, calculate magnetic heading. By default true heading is returned.
+-- @param Core.Point#COORDINATE coord (Optional) Coordinate from which heading is calculated. Default is current carrier position.
+-- @return #number Carrier heading in degrees.
+function AIRBOSS:GetHeadingIntoWind_old( vdeck, magnetic, coord )
local function adjustDegreesForWindSpeed(windSpeed)
local degreesAdjustment = 0
@@ -11608,7 +11649,13 @@ function AIRBOSS:GetHeadingIntoWind_old( magnetic, coord )
intowind = intowind + 360
end
- return intowind
+ -- Wind speed.
+ --local _, vwind = self:GetWind()
+
+ -- Speed of carrier in m/s but at least 4 knots.
+ local vtot = math.max(vdeck-UTILS.MpsToKnots(vwind), 4)
+
+ return intowind, vtot
end
--- Get true (or magnetic) heading of carrier into the wind. This accounts for the angled runway.
@@ -11619,7 +11666,7 @@ end
-- @param Core.Point#COORDINATE coord (Optional) Coordinate from which heading is calculated. Default is current carrier position.
-- @return #number Carrier heading in degrees.
-- @return #number Carrier speed in knots to reach desired wind speed on deck.
-function AIRBOSS:GetHeadingIntoWind( vdeck, magnetic, coord )
+function AIRBOSS:GetHeadingIntoWind_new( vdeck, magnetic, coord )
-- Default offset angle.
local Offset=self.carrierparam.rwyangle or 0
@@ -14280,6 +14327,8 @@ function AIRBOSS:_GetACNickname( actype )
nickname = "Harrier"
elseif actype == AIRBOSS.AircraftCarrier.E2D then
nickname = "Hawkeye"
+ elseif actype == AIRBOSS.AircraftCarrier.C2A then
+ nickname = "Greyhound"
elseif actype == AIRBOSS.AircraftCarrier.F14A_AI or actype == AIRBOSS.AircraftCarrier.F14A or actype == AIRBOSS.AircraftCarrier.F14B then
nickname = "Tomcat"
elseif actype == AIRBOSS.AircraftCarrier.FA18C or actype == AIRBOSS.AircraftCarrier.HORNET then
@@ -14317,32 +14366,55 @@ function AIRBOSS:_GetOnboardNumbers( group, playeronly )
-- Debug text.
local text = string.format( "Onboard numbers of group %s:", groupname )
- -- Units of template group.
- local units = group:GetTemplate().units
+ local template=group:GetTemplate()
- -- Get numbers.
local numbers = {}
- for _, unit in pairs( units ) do
+ if template then
- -- Onboard number and unit name.
- local n = tostring( unit.onboard_num )
- local name = unit.name
- local skill = unit.skill or "Unknown"
+ -- Units of template group.
+ local units = template.units
- -- Debug text.
- text = text .. string.format( "\n- unit %s: onboard #=%s skill=%s", name, n, tostring( skill ) )
+ -- Get numbers.
+ for _, unit in pairs( units ) do
- if playeronly and skill == "Client" or skill == "Player" then
- -- There can be only one player in the group, so we skip everything else.
- return n
+ -- Onboard number and unit name.
+ local n = tostring( unit.onboard_num )
+ local name = unit.name
+ local skill = unit.skill or "Unknown"
+
+ -- Debug text.
+ text = text .. string.format( "\n- unit %s: onboard #=%s skill=%s", name, n, tostring( skill ) )
+
+ if playeronly and skill == "Client" or skill == "Player" then
+ -- There can be only one player in the group, so we skip everything else.
+ return n
+ end
+
+ -- Table entry.
+ numbers[name] = n
end
- -- Table entry.
- numbers[name] = n
- end
+ -- Debug info.
+ self:T2( self.lid .. text )
- -- Debug info.
- self:T2( self.lid .. text )
+ else
+
+ if playeronly then
+ return 101
+ else
+
+ local units=group:GetUnits()
+
+ for i,_unit in pairs(units) do
+ local name=_unit:GetName()
+
+ numbers[name]=100+i
+
+ end
+
+ end
+
+ end
return numbers
end
diff --git a/Moose Development/Moose/Ops/ArmyGroup.lua b/Moose Development/Moose/Ops/ArmyGroup.lua
index 4dff8027c..62f3dfec4 100644
--- a/Moose Development/Moose/Ops/ArmyGroup.lua
+++ b/Moose Development/Moose/Ops/ArmyGroup.lua
@@ -2109,7 +2109,7 @@ function ARMYGROUP:_InitGroup(Template, Delay)
return
end
- self:I(self.lid.."FF Initializing Group")
+ self:T(self.lid.."FF Initializing Group")
-- Get template of group.
local template=Template or self:_GetTemplate()
diff --git a/Moose Development/Moose/Ops/CSAR.lua b/Moose Development/Moose/Ops/CSAR.lua
index 905798115..7b716ed5c 100644
--- a/Moose Development/Moose/Ops/CSAR.lua
+++ b/Moose Development/Moose/Ops/CSAR.lua
@@ -291,10 +291,12 @@ CSAR.AircraftType["UH-60L"] = 10
CSAR.AircraftType["AH-64D_BLK_II"] = 2
CSAR.AircraftType["Bronco-OV-10A"] = 2
CSAR.AircraftType["MH-60R"] = 10
+CSAR.AircraftType["OH-6A"] = 2
+CSAR.AircraftType["OH58D"] = 2
--- CSAR class version.
-- @field #string version
-CSAR.version="1.0.21"
+CSAR.version="1.0.24"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
@@ -734,7 +736,7 @@ function CSAR:_SpawnPilotInField(country,point,frequency,wetfeet)
:NewWithAlias(template,alias)
:InitCoalition(coalition)
:InitCountry(country)
- :InitAIOnOff(pilotcacontrol)
+ --:InitAIOnOff(pilotcacontrol)
:InitDelayOff()
:SpawnFromCoordinate(point)
@@ -1238,10 +1240,24 @@ function CSAR:_InitSARForPilot(_downedGroup, _GroupName, _freq, _nomessage, _pla
if not _nomessage then
if _freq ~= 0 then --shagrat
local _text = string.format("%s requests SAR at %s, beacon at %.2f KHz", _groupName, _coordinatesText, _freqk)--shagrat _groupName to prevent 'f15_Pilot_Parachute'
- self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
+ if self.coordtype ~= 2 then --not MGRS
+ self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
+ else
+ self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true)
+ local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true)
+ local _text = string.format("%s requests SAR at %s, beacon at %.2f kilo hertz", _groupName, coordtext, _freqk)
+ self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false)
+ end
else --shagrat CASEVAC msg
local _text = string.format("Pickup Zone at %s.", _coordinatesText )
- self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
+ if self.coordtype ~= 2 then --not MGRS
+ self:_DisplayToAllSAR(_text,self.coalition,self.messageTime)
+ else
+ self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,false,true)
+ local coordtext = UTILS.MGRSStringToSRSFriendly(_coordinatesText,true)
+ local _text = string.format("Pickup Zone at %s.", coordtext )
+ self:_DisplayToAllSAR(_text,self.coalition,self.messageTime,true,false)
+ end
end
end
@@ -1945,23 +1961,28 @@ end
--- (Internal) Display info to all SAR groups.
-- @param #CSAR self
-- @param #string _message Message to display.
--- @param #number _side Coalition of message.
+-- @param #number _side Coalition of message.
-- @param #number _messagetime How long to show.
-function CSAR:_DisplayToAllSAR(_message, _side, _messagetime)
+-- @param #boolean ToSRS If true or nil, send to SRS TTS
+-- @param #boolean ToScreen If true or nil, send to Screen
+function CSAR:_DisplayToAllSAR(_message, _side, _messagetime,ToSRS,ToScreen)
self:T(self.lid .. " _DisplayToAllSAR")
local messagetime = _messagetime or self.messageTime
- if self.msrs then
+ self:T({_message,ToSRS=ToSRS,ToScreen=ToScreen})
+ if self.msrs and (ToSRS == true or ToSRS == nil) then
local voice = self.CSARVoice or MSRS.Voices.Google.Standard.en_GB_Standard_F
if self.msrs:GetProvider() == MSRS.Provider.WINDOWS then
voice = self.CSARVoiceMS or MSRS.Voices.Microsoft.Hedda
end
- self:I("Voice = "..voice)
+ self:F("Voice = "..voice)
self.SRSQueue:NewTransmission(_message,duration,self.msrs,tstart,2,subgroups,subtitle,subduration,self.SRSchannel,self.SRSModulation,gender,culture,voice,volume,label,self.coordinate)
end
- for _, _unitName in pairs(self.csarUnits) do
- local _unit = self:_GetSARHeli(_unitName)
- if _unit and not self.suppressmessages then
- self:_DisplayMessageToSAR(_unit, _message, _messagetime)
+ if ToScreen == true or ToScreen == nil then
+ for _, _unitName in pairs(self.csarUnits) do
+ local _unit = self:_GetSARHeli(_unitName)
+ if _unit and not self.suppressmessages then
+ self:_DisplayMessageToSAR(_unit, _message, _messagetime)
+ end
end
end
return self
diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua
index 65db26ed8..037446ea4 100644
--- a/Moose Development/Moose/Ops/CTLD.lua
+++ b/Moose Development/Moose/Ops/CTLD.lua
@@ -1249,11 +1249,13 @@ CTLD.UnitTypeCapabilities = {
["SH-60B"] = {type="SH-60B", crates=true, troops=true, cratelimit = 2, trooplimit = 20, length = 16, cargoweightlimit = 3500}, -- 4t cargo, 20 (unsec) seats
["AH-64D_BLK_II"] = {type="AH-64D_BLK_II", crates=false, troops=true, cratelimit = 0, trooplimit = 2, length = 17, cargoweightlimit = 200}, -- 2 ppl **outside** the helo
["Bronco-OV-10A"] = {type="Bronco-OV-10A", crates= false, troops=true, cratelimit = 0, trooplimit = 5, length = 13, cargoweightlimit = 1450},
+ ["OH-6A"] = {type="OH-6A", crates=false, troops=true, cratelimit = 0, trooplimit = 4, length = 7, cargoweightlimit = 550},
+ ["OH58D"] = {type="OH58D", crates=false, troops=false, cratelimit = 0, trooplimit = 0, length = 14, cargoweightlimit = 400},
}
--- CTLD class version.
-- @field #string version
-CTLD.version="1.0.51"
+CTLD.version="1.0.54"
--- Instantiate a new CTLD.
-- @param #CTLD self
@@ -3607,7 +3609,7 @@ function CTLD:_MoveGroupToZone(Group)
local groupcoord = Group:GetCoordinate()
-- Get closest zone of type
local outcome, name, zone, distance = self:IsUnitInZone(Group,CTLD.CargoZoneType.MOVE)
- if (distance <= self.movetroopsdistance) and zone then
+ if (distance <= self.movetroopsdistance) and outcome == true and zone~= nil then
-- yes, we can ;)
local groupname = Group:GetName()
local zonecoord = zone:GetRandomCoordinate(20,125) -- Core.Point#COORDINATE
@@ -4464,10 +4466,9 @@ function CTLD:IsUnitInZone(Unit,Zonetype)
zonewidth = zoneradius
end
local distance = self:_GetDistance(zonecoord,unitcoord)
- if zone:IsVec2InZone(unitVec2) and active then
+ self:T("Distance Zone: "..distance)
+ if (zone:IsVec2InZone(unitVec2) or Zonetype == CTLD.CargoZoneType.MOVE) and active == true and maxdist > distance then
outcome = true
- end
- if maxdist > distance then
maxdist = distance
zoneret = zone
zonenameret = zonename
diff --git a/Moose Development/Moose/Ops/Chief.lua b/Moose Development/Moose/Ops/Chief.lua
index e1027924b..b5ed5647c 100644
--- a/Moose Development/Moose/Ops/Chief.lua
+++ b/Moose Development/Moose/Ops/Chief.lua
@@ -1078,6 +1078,13 @@ function CHIEF:SetStrategy(Strategy)
return self
end
+--- Get current strategy.
+-- @param #CHIEF self
+-- @return #string Strategy.
+function CHIEF:GetStrategy()
+ return self.strategy
+end
+
--- Get defence condition.
-- @param #CHIEF self
-- @param #string Current Defence condition. See @{#CHIEF.DEFCON}, e.g. `CHIEF.DEFCON.RED`.
@@ -1456,7 +1463,7 @@ end
--- Add a CAP zone. Flights will engage detected targets inside this zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone CAP Zone. Has to be a circular zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
@@ -1472,7 +1479,7 @@ end
--- Add a GCI CAP.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone, where the flight orbits.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
@@ -1499,7 +1506,7 @@ end
--- Add an AWACS zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
@@ -1526,7 +1533,7 @@ end
--- Add a refuelling tanker zone.
-- @param #CHIEF self
-- @param Core.Zone#ZONE Zone Zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
diff --git a/Moose Development/Moose/Ops/Commander.lua b/Moose Development/Moose/Ops/Commander.lua
index 1c649dd85..596eb5661 100644
--- a/Moose Development/Moose/Ops/Commander.lua
+++ b/Moose Development/Moose/Ops/Commander.lua
@@ -663,7 +663,7 @@ end
--- Add a CAP zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone CapZone Zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
@@ -689,7 +689,7 @@ end
--- Add a GCICAP zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone CapZone Zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
@@ -735,7 +735,7 @@ end
--- Add an AWACS zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
@@ -782,7 +782,7 @@ end
--- Add a refuelling tanker zone.
-- @param #COMMANDER self
-- @param Core.Zone#ZONE Zone Zone.
--- @param #number Altitude Orbit altitude in feet. Default is 12,0000 feet.
+-- @param #number Altitude Orbit altitude in feet. Default is 12,000 feet.
-- @param #number Speed Orbit speed in KIAS. Default 350 kts.
-- @param #number Heading Heading of race-track pattern in degrees. Default 270 (East to West).
-- @param #number Leg Length of race-track in NM. Default 30 NM.
diff --git a/Moose Development/Moose/Ops/FlightControl.lua b/Moose Development/Moose/Ops/FlightControl.lua
index b1eb9f0c2..656c87c2e 100644
--- a/Moose Development/Moose/Ops/FlightControl.lua
+++ b/Moose Development/Moose/Ops/FlightControl.lua
@@ -62,6 +62,9 @@
-- @field #number runwayrepairtime Time in seconds until runway will be repaired after it was destroyed. Default is 3600 sec (one hour).
-- @field #boolean markerParking If `true`, occupied parking spots are marked.
-- @field #table warnings Warnings issued to flight groups.
+-- @field #boolean nosubs If `true`, SRS TTS is without subtitles.
+-- @field #number Nplayers Number of human players. Updated at each StatusUpdate call.
+-- @field #boolean radioOnlyIfPlayers Activate to limit transmissions only if players are active at the airbase.
-- @extends Core.Fsm#FSM
--- **Ground Control**: Airliner X, Good news, you are clear to taxi to the active.
@@ -273,6 +276,7 @@ FLIGHTCONTROL = {
hpcounter = 0,
warnings = {},
nosubs = false,
+ Nplayers = 0,
}
--- Holding point. Contains holding stacks.
@@ -351,7 +355,7 @@ FLIGHTCONTROL.Violation={
--- FlightControl class version.
-- @field #string version
-FLIGHTCONTROL.version="0.7.5"
+FLIGHTCONTROL.version="0.7.7"
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO list
@@ -452,6 +456,16 @@ function FLIGHTCONTROL:New(AirbaseName, Frequency, Modulation, PathToSRS, Port,
-- Init msrs queue.
self.msrsqueue=MSRSQUEUE:New(self.alias)
+ -- Set that transmission is only if alive players on the server.
+ self:SetTransmitOnlyWithPlayers(true)
+
+ -- Init msrs bases
+ local path = PathToSRS or MSRS.path
+ local port = Port or MSRS.port or 5002
+
+ -- Set SRS Port
+ self:SetSRSPort(port)
+
-- SRS for Tower.
self.msrsTower=MSRS:New(path, Frequency, Modulation)
self.msrsTower:SetPort(port)
@@ -605,6 +619,31 @@ function FLIGHTCONTROL:SetVerbosity(VerbosityLevel)
return self
end
+--- Limit radio transmissions only if human players are registered at the airbase.
+-- This can be used to reduce TTS messages on heavy missions.
+-- @param #FLIGHTCONTROL self
+-- @param #boolean Switch If `true` or `nil` no transmission if there are no players. Use `false` enable TTS with no players.
+-- @return #FLIGHTCONTROL self
+function FLIGHTCONTROL:SetRadioOnlyIfPlayers(Switch)
+ if Switch==nil or Switch==true then
+ self.radioOnlyIfPlayers=true
+ else
+ self.radioOnlyIfPlayers=false
+ end
+ return self
+end
+
+
+--- Set whether to only transmit TTS messages if there are players on the server.
+-- @param #FLIGHTCONTROL self
+-- @param #boolean Switch If `true`, only send TTS messages if there are alive Players. If `false` or `nil`, transmission are done also if no players are on the server.
+-- @return #FLIGHTCONTROL self
+function FLIGHTCONTROL:SetTransmitOnlyWithPlayers(Switch)
+ self.msrsqueue:SetTransmitOnlyWithPlayers(Switch)
+ return self
+end
+
+
--- Set subtitles to appear on SRS TTS messages.
-- @param #FLIGHTCONTROL self
-- @return #FLIGHTCONTROL self
@@ -2242,7 +2281,7 @@ function FLIGHTCONTROL:_InitParkingSpots()
local unitname=unit and unit:GetName() or "unknown"
local isalive=unit:IsAlive()
-
+
self:T2(self.lid..string.format("FF parking spot %d is occupied by unit %s alive=%s", spot.TerminalID, unitname, tostring(isalive)))
if isalive then
@@ -4243,6 +4282,72 @@ function FLIGHTCONTROL:_CheckFlights()
end
end
+ -- Count number of players
+ self.Nplayers=0
+ for _,_flight in pairs(self.flights) do
+ local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
+ if not flight.isAI then
+ self.Nplayers=self.Nplayers+1
+ end
+ end
+
+ -- Check speeding.
+ if self.speedLimitTaxi then
+
+ for _,_flight in pairs(self.flights) do
+ local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
+
+ if not flight.isAI then
+
+ -- Get player element.
+ local playerElement=flight:GetPlayerElement()
+
+ -- Current flight status.
+ local flightstatus=self:GetFlightStatus(flight)
+
+ if playerElement then
+
+ -- Check if speeding while taxiing.
+ if (flightstatus==FLIGHTCONTROL.FlightStatus.TAXIINB or flightstatus==FLIGHTCONTROL.FlightStatus.TAXIOUT) and self.speedLimitTaxi then
+
+ -- Current speed in m/s.
+ local speed=playerElement.unit:GetVelocityMPS()
+
+ -- Current position.
+ local coord=playerElement.unit:GetCoord()
+
+ -- We do not want to check speed on runways.
+ local onRunway=self:IsCoordinateRunway(coord)
+
+ -- Debug output.
+ self:T(self.lid..string.format("Player %s speed %.1f knots (max=%.1f) onRunway=%s", playerElement.playerName, UTILS.MpsToKnots(speed), UTILS.MpsToKnots(self.speedLimitTaxi), tostring(onRunway)))
+
+ if speed and speed>self.speedLimitTaxi and not onRunway then
+
+ -- Callsign.
+ local callsign=self:_GetCallsignName(flight)
+
+ -- Radio text.
+ local text=string.format("%s, slow down, you are taxiing too fast!", callsign)
+
+ -- Radio message to player.
+ self:TransmissionTower(text, flight)
+
+ -- Get player data.
+ local PlayerData=flight:_GetPlayerData()
+
+ -- Trigger FSM speeding event.
+ self:PlayerSpeeding(PlayerData)
+
+ end
+
+ end
+
+ end
+ end
+ end
+ end
+
-- Loop over all flights.
for _,_flight in pairs(self.flights) do
local flight=_flight --Ops.FlightGroup#FLIGHTGROUP
@@ -4749,6 +4854,11 @@ end
-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec.
function FLIGHTCONTROL:TransmissionTower(Text, Flight, Delay)
+ if self.radioOnlyIfPlayers==true and self.Nplayers==0 then
+ self:T(self.lid.."No players ==> skipping TOWER radio transmission")
+ return
+ end
+
-- Spoken text.
local text=self:_GetTextForSpeech(Text)
@@ -4815,6 +4925,12 @@ end
-- @param #number Delay Delay in seconds before the text is transmitted. Default 0 sec.
function FLIGHTCONTROL:TransmissionPilot(Text, Flight, Delay)
+ if self.radioOnlyIfPlayers==true and self.Nplayers==0 then
+ self:T(self.lid.."No players ==> skipping PILOT radio transmission")
+ return
+ end
+
+
-- Get player data.
local playerData=Flight:_GetPlayerData()
diff --git a/Moose Development/Moose/Ops/FlightGroup.lua b/Moose Development/Moose/Ops/FlightGroup.lua
index 19b818704..886ac9c23 100644
--- a/Moose Development/Moose/Ops/FlightGroup.lua
+++ b/Moose Development/Moose/Ops/FlightGroup.lua
@@ -2831,6 +2831,11 @@ function FLIGHTGROUP:_CheckGroupDone(delay, waittime)
self:T(self.lid.."Engaging! Group NOT done...")
return
end
+ -- Check if group is going for fuel.
+ if self:IsGoing4Fuel() then
+ self:T(self.lid.."Going for FUEL! Group NOT done...")
+ return
+ end
-- Number of tasks remaining.
local nTasks=self:CountRemainingTasks()
@@ -3447,6 +3452,9 @@ function FLIGHTGROUP:onafterRefuel(From, Event, To, Coordinate)
self:Route({wp0, wp9}, 1)
+ -- Set RTB on Bingo option. Currently DCS does not execute the refueling task if RTB_ON_BINGO is set to "NO RTB ON BINGO"
+ self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, true)
+
end
--- On after "Refueled" event.
@@ -3460,6 +3468,9 @@ function FLIGHTGROUP:onafterRefueled(From, Event, To)
local text=string.format("Flight group finished refuelling")
self:T(self.lid..text)
+ -- Set RTB on Bingo option to "NO RTB ON BINGO"
+ self.group:SetOption(AI.Option.Air.id.RTB_ON_BINGO, false)
+
-- Check if flight is done.
self:_CheckGroupDone(1)
diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua
index e0c7f8c60..2f02d9ce3 100644
--- a/Moose Development/Moose/Ops/PlayerTask.lua
+++ b/Moose Development/Moose/Ops/PlayerTask.lua
@@ -21,7 +21,7 @@
-- ===
-- @module Ops.PlayerTask
-- @image OPS_PlayerTask.jpg
--- @date Last Update Feb 2024
+-- @date Last Update May 2024
do
@@ -1213,6 +1213,9 @@ do
-- AIRDEFENSE = "Airdefense",
-- SAM = "SAM",
-- GROUP = "Group",
+-- ELEVATION = "\nTarget Elevation: %s %s",
+-- METER = "meter",
+-- FEET = "feet",
-- },
--
-- e.g.
@@ -1367,7 +1370,7 @@ PLAYERTASKCONTROLLER.Type = {
AUFTRAG.Type.PRECISIONBOMBING = "Precision Bombing"
AUFTRAG.Type.CTLD = "Combat Transport"
AUFTRAG.Type.CSAR = "Combat Rescue"
-
+AUFTRAG.Type.CONQUER = "Conquer"
---
-- @type Scores
PLAYERTASKCONTROLLER.Scores = {
@@ -1380,7 +1383,8 @@ PLAYERTASKCONTROLLER.Scores = {
[AUFTRAG.Type.BAI] = 100,
[AUFTRAG.Type.SEAD] = 100,
[AUFTRAG.Type.BOMBING] = 100,
- [AUFTRAG.Type.BOMBRUNWAY] = 100,
+ [AUFTRAG.Type.BOMBRUNWAY] = 100,
+ [AUFTRAG.Type.CONQUER] = 100,
}
---
@@ -1419,6 +1423,9 @@ PLAYERTASKCONTROLLER.Messages = {
THREATMEDIUM = "medium",
THREATLOW = "low",
THREATTEXT = "%s\nThreat: %s\nTargets left: %d\nCoord: %s",
+ ELEVATION = "\nTarget Elevation: %s %s",
+ METER = "meter",
+ FEET = "feet",
THREATTEXTTTS = "%s, %s. Target information for %s. Threat level %s. Targets left %d. Target location %s.",
MARKTASK = "%s, %s, copy, task %03d location marked on map!",
SMOKETASK = "%s, %s, copy, task %03d location smoked!",
@@ -1499,6 +1506,9 @@ PLAYERTASKCONTROLLER.Messages = {
THREATMEDIUM = "mittel",
THREATLOW = "niedrig",
THREATTEXT = "%s\nGefahrstufe: %s\nZiele: %d\nKoord: %s",
+ ELEVATION = "\nZiel Höhe: %s %s",
+ METER = "Meter",
+ FEET = "Fuss",
THREATTEXTTTS = "%s, %s. Zielinformation zu %s. Gefahrstufe %s. Ziele %d. Zielposition %s.",
MARKTASK = "%s, %s, verstanden, Zielposition %03d auf der Karte markiert!",
SMOKETASK = "%s, %s, verstanden, Zielposition %03d mit Rauch markiert!",
@@ -1561,7 +1571,7 @@ PLAYERTASKCONTROLLER.Messages = {
--- PLAYERTASK class version.
-- @field #string version
-PLAYERTASKCONTROLLER.version="0.1.65"
+PLAYERTASKCONTROLLER.version="0.1.66"
--- Create and run a new TASKCONTROLLER instance.
-- @param #PLAYERTASKCONTROLLER self
@@ -2113,10 +2123,12 @@ function PLAYERTASKCONTROLLER:_GetPlayerName(Client)
local ttsplayername = nil
if not self.customcallsigns[playername] then
local playergroup = Client:GetGroup()
- ttsplayername = playergroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations)
- local newplayername = self:_GetTextForSpeech(ttsplayername)
- self.customcallsigns[playername] = newplayername
- ttsplayername = newplayername
+ if playergroup ~= nil then
+ ttsplayername = playergroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber,self.CallsignTranslations)
+ local newplayername = self:_GetTextForSpeech(ttsplayername)
+ self.customcallsigns[playername] = newplayername
+ ttsplayername = newplayername
+ end
else
ttsplayername = self.customcallsigns[playername]
end
@@ -3182,7 +3194,8 @@ function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client)
local ttsname = self.gettext:GetEntry("TASKNAMETTS",self.locale)
local taskname = string.format(tname,task.Type,task.PlayerTaskNr)
local ttstaskname = string.format(ttsname,task.TTSType,task.PlayerTaskNr)
- local Coordinate = task.Target:GetCoordinate() or COORDINATE:New(0,0,0)
+ local Coordinate = task.Target:GetCoordinate() or COORDINATE:New(0,0,0) -- Core.Point#COORDINATE
+ local Elevation = Coordinate:GetLandHeight() or 0 -- meters
local CoordText = ""
local CoordTextLLDM = nil
if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then
@@ -3207,6 +3220,17 @@ function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client)
local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel
local ThreatLocaleText = self.gettext:GetEntry("THREATTEXT",self.locale)
text = string.format(ThreatLocaleText, taskname, ThreatGraph, targets, CoordText)
+ local settings = _DATABASE:GetPlayerSettings(playername) or _SETTINGS -- Core.Settings#SETTINGS
+ local elevationmeasure = self.gettext:GetEntry("FEET",self.locale)
+ if settings:IsMetric() then
+ elevationmeasure = self.gettext:GetEntry("METER",self.locale)
+ --Elevation = math.floor(UTILS.MetersToFeet(Elevation))
+ else
+ 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)
-- Prec bombing
if task.Type == AUFTRAG.Type.PRECISIONBOMBING and self.precisionbombing then
if self.LasingDrone and self.LasingDrone.playertask then
diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua
index b890206c6..827627aca 100644
--- a/Moose Development/Moose/Utilities/Utils.lua
+++ b/Moose Development/Moose/Utilities/Utils.lua
@@ -55,6 +55,7 @@ BIGSMOKEPRESET = {
-- @field #string MarianaIslands Mariana Islands map.
-- @field #string Falklands South Atlantic map.
-- @field #string Sinai Sinai map.
+-- @field #string Kola Kola map.
DCSMAP = {
Caucasus="Caucasus",
NTTR="Nevada",
@@ -64,7 +65,8 @@ DCSMAP = {
Syria="Syria",
MarianaIslands="MarianaIslands",
Falklands="Falklands",
- Sinai="SinaiMap"
+ Sinai="SinaiMap",
+ Kola="Kola"
}
@@ -102,7 +104,7 @@ CALLSIGN={
Shell=3,
Navy_One=4,
Mauler=5,
- Bloodhound=6,
+ Bloodhound=6,
},
-- JTAC
JTAC={
@@ -416,7 +418,7 @@ function UTILS._OneLineSerialize(tbl)
end
end
-
+
tbl_str[#tbl_str + 1] = '}'
return table.concat(tbl_str)
else
@@ -433,7 +435,7 @@ UTILS.BasicSerialize = function(s)
if ((type(s) == 'number') or (type(s) == 'boolean') or (type(s) == 'function') or (type(s) == 'userdata') ) then
return tostring(s)
elseif type(s) == "table" then
- return UTILS._OneLineSerialize(s)
+ return UTILS._OneLineSerialize(s)
elseif type(s) == 'string' then
s = string.format('(%s)', s)
return s
@@ -562,15 +564,15 @@ end
-- @param #string fname File name.
function UTILS.Gdump(fname)
if lfs and io then
-
+
local fdir = lfs.writedir() .. [[Logs\]] .. fname
-
+
local f = io.open(fdir, 'w')
-
+
f:write(UTILS.TableShow(_G))
-
+
f:close()
-
+
env.info(string.format('Wrote debug data to $1', fdir))
else
env.error("WARNING: lfs and/or io not de-sanitized - cannot dump _G!")
@@ -894,17 +896,17 @@ UTILS.tostringLLM2KData = function( lat, lon, acc)
-- degrees, decimal minutes.
latMin = UTILS.Round(latMin, acc)
lonMin = UTILS.Round(lonMin, acc)
-
+
if latMin == 60 then
latMin = 0
latDeg = latDeg + 1
end
-
+
if lonMin == 60 then
lonMin = 0
lonDeg = lonDeg + 1
end
-
+
local minFrmtStr -- create the formatting string for the minutes place
if acc <= 0 then -- no decimal place.
minFrmtStr = '%02d'
@@ -912,7 +914,7 @@ UTILS.tostringLLM2KData = function( lat, lon, acc)
local width = 3 + acc -- 01.310 - that's a width of 6, for example.
minFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
end
-
+
-- 024 23'N or 024 23.123'N
return latHemi..string.format('%02d:', latDeg) .. string.format(minFrmtStr, latMin), lonHemi..string.format('%02d:', lonDeg) .. string.format(minFrmtStr, lonMin)
@@ -924,9 +926,9 @@ UTILS.tostringMGRS = function(MGRS, acc) --R2.1
if acc <= 0 then
return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
else
-
+
if acc > 5 then acc = 5 end
-
+
-- Test if Easting/Northing have less than 4 digits.
--MGRS.Easting=123 -- should be 00123
--MGRS.Northing=5432 -- should be 05432
@@ -1409,7 +1411,7 @@ end
function UTILS.VecDist2D(a, b)
local d = math.huge
-
+
if (not a) or (not b) then return d end
local c={x=b.x-a.x, y=b.y-a.y}
@@ -1425,12 +1427,12 @@ end
-- @param DCS#Vec3 b Vector in 3D with x, y, z components.
-- @return #number Distance between the vectors.
function UTILS.VecDist3D(a, b)
-
-
+
+
local d = math.huge
-
+
if (not a) or (not b) then return d end
-
+
local c={x=b.x-a.x, y=b.y-a.y, z=b.z-a.z}
d=math.sqrt(UTILS.VecDot(c, c))
@@ -1778,6 +1780,7 @@ end
-- * Mariana Islands +2 (East)
-- * Falklands +12 (East) - note there's a LOT of deviation across the map, as we're closer to the South Pole
-- * Sinai +4.8 (East)
+-- * Kola +15 (East) - not there is a lot of deviation across the map (-1° to +24°), as we are close to the North pole
-- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre
-- @return #number Declination in degrees.
function UTILS.GetMagneticDeclination(map)
@@ -1804,6 +1807,8 @@ function UTILS.GetMagneticDeclination(map)
declination=12
elseif map==DCSMAP.Sinai then
declination=4.8
+ elseif map==DCSMAP.Kola then
+ declination=15
else
declination=0
end
@@ -1871,7 +1876,7 @@ function UTILS.GetCoalitionEnemy(Coalition, Neutral)
local Coalitions={}
if Coalition then
- if Coalition==coalition.side.RED then
+ if Coalition==coalition.side.RED then
Coalitions={coalition.side.BLUE}
elseif Coalition==coalition.side.BLUE then
Coalitions={coalition.side.RED}
@@ -1879,7 +1884,7 @@ function UTILS.GetCoalitionEnemy(Coalition, Neutral)
Coalitions={coalition.side.RED, coalition.side.BLUE}
end
end
-
+
if Neutral then
table.insert(Coalitions, coalition.side.NEUTRAL)
end
@@ -1910,17 +1915,17 @@ end
-- @param #number Typename The type name.
-- @return #string The Reporting name or "Bogey".
function UTILS.GetReportingName(Typename)
-
+
local typename = string.lower(Typename)
-
+
for name, value in pairs(ENUMS.ReportingName.NATO) do
local svalue = string.lower(value)
if string.find(typename,svalue,1,true) then
return name
end
end
-
- return "Bogey"
+
+ return "Bogey"
end
--- Get the callsign name from its enumerator value
@@ -1951,49 +1956,49 @@ function UTILS.GetCallsignName(Callsign)
return name
end
end
-
+
for name, value in pairs(CALLSIGN.B1B) do
if value==Callsign then
return name
end
end
-
+
for name, value in pairs(CALLSIGN.B52) do
if value==Callsign then
return name
end
end
-
+
for name, value in pairs(CALLSIGN.F15E) do
if value==Callsign then
return name
end
end
-
+
for name, value in pairs(CALLSIGN.F16) do
if value==Callsign then
return name
end
end
-
+
for name, value in pairs(CALLSIGN.F18) do
if value==Callsign then
return name
end
end
-
+
for name, value in pairs(CALLSIGN.FARP) do
if value==Callsign then
return name
end
end
-
+
for name, value in pairs(CALLSIGN.TransportAircraft) do
if value==Callsign then
return name
end
end
-
+
return "Ghostrider"
end
@@ -2020,7 +2025,9 @@ function UTILS.GMTToLocalTimeDifference()
elseif theatre==DCSMAP.Falklands then
return -3 -- Fireland is UTC-3 hours.
elseif theatre==DCSMAP.Sinai then
- return 2 -- Currently map is +2 but should be +3 (DCS bug?)
+ return 2 -- Currently map is +2 but should be +3 (DCS bug?)
+ elseif theatre==DCSMAP.Kola then
+ return 3 -- Currently map is +2 but should be +3 (DCS bug?)
else
BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre)))
return 0
@@ -2225,19 +2232,19 @@ function UTILS.GetRandomTableElement(t, replace)
BASE:I("Error in ShuffleTable: Missing or wrong type of Argument")
return
end
-
+
math.random()
math.random()
math.random()
-
+
local r=math.random(#t)
-
+
local element=t[r]
-
+
if not replace then
table.remove(t, r)
end
-
+
return element
end
@@ -2266,7 +2273,7 @@ function UTILS.IsLoadingDoorOpen( unit_name )
BASE:T(unit_name .. " a side door is open ")
return true
end
-
+
if string.find(type_name, "SA342" ) and (unit:getDrawArgumentValue(34) == 1) then
BASE:T(unit_name .. " front door(s) are open or doors removed")
return true
@@ -2291,7 +2298,7 @@ function UTILS.IsLoadingDoorOpen( unit_name )
BASE:T(unit_name .. " door is open")
return true
end
-
+
if type_name == "UH-60L" and (unit:getDrawArgumentValue(401) == 1 or unit:getDrawArgumentValue(402) == 1) then
BASE:T(unit_name .. " cargo door is open")
return true
@@ -2301,22 +2308,27 @@ function UTILS.IsLoadingDoorOpen( unit_name )
BASE:T(unit_name .. " front door(s) are open")
return true
end
-
+
if type_name == "AH-64D_BLK_II" then
BASE:T(unit_name .. " front door(s) are open")
return true -- no doors on this one ;)
end
-
+
if type_name == "Bronco-OV-10A" then
BASE:T(unit_name .. " front door(s) are open")
return true -- no doors on this one ;)
end
-
+
if type_name == "MH-60R" and (unit:getDrawArgumentValue(403) > 0 or unit:getDrawArgumentValue(403) == -1) then
BASE:T(unit_name .. " cargo door is open")
return true
end
-
+
+ if type_name == " OH-58D" and (unit:getDrawArgumentValue(35) > 0 or unit:getDrawArgumentValue(421) == -1) then
+ BASE:T(unit_name .. " cargo door is open")
+ return true
+ end
+
return false
end -- nil
@@ -2425,7 +2437,7 @@ function UTILS.GenerateUHFrequencies(Start,End)
local FreeUHFFrequencies = {}
local _start = 220000000
-
+
if not Start then
while _start < 399000000 do
if _start ~= 243000000 then
@@ -2436,7 +2448,7 @@ function UTILS.GenerateUHFrequencies(Start,End)
else
local myend = End*1000000 or 399000000
local mystart = Start*1000000 or 220000000
-
+
while _start < 399000000 do
if _start ~= 243000000 and (_start < mystart or _start > myend) then
print(_start)
@@ -2444,10 +2456,10 @@ function UTILS.GenerateUHFrequencies(Start,End)
end
_start = _start + 500000
end
-
+
end
-
-
+
+
return FreeUHFFrequencies
end
@@ -2488,7 +2500,7 @@ function UTILS.GenerateLaserCodes()
return jtacGeneratedLaserCodes
end
---- Ensure the passed object is a table.
+--- Ensure the passed object is a table.
-- @param #table Object The object that should be a table.
-- @param #boolean ReturnNil If `true`, return `#nil` if `Object` is nil. Otherwise an empty table `{}` is returned.
-- @return #table The object that now certainly *is* a table.
@@ -2500,11 +2512,11 @@ function UTILS.EnsureTable(Object, ReturnNil)
end
else
if ReturnNil then
- return nil
+ return nil
else
- Object={}
+ Object={}
end
-
+
end
return Object
@@ -2516,30 +2528,30 @@ end
-- @param #table Data The LUA data structure to save. This will be e.g. a table of text lines with an \\n at the end of each line.
-- @return #boolean outcome True if saving is possible, else false.
function UTILS.SaveToFile(Path,Filename,Data)
- -- Thanks to @FunkyFranky
+ -- Thanks to @FunkyFranky
-- Check io module is available.
if not io then
BASE:E("ERROR: io not desanitized. Can't save current file.")
return false
end
-
+
-- Check default path.
if Path==nil and not lfs then
BASE:E("WARNING: lfs not desanitized. File will be saved in DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
end
-
+
-- Set path or default.
local path = nil
if lfs then
path=Path or lfs.writedir()
end
-
+
-- Set file name.
local filename=Filename
if path~=nil then
filename=path.."\\"..filename
end
-
+
-- write
local f = assert(io.open(filename, "wb"))
f:write(Data)
@@ -2547,43 +2559,43 @@ function UTILS.SaveToFile(Path,Filename,Data)
return true
end
---- Function to save an object to a file
+--- Function to load an object from a file.
-- @param #string Path The path to use. Use double backslashes \\\\ on Windows filesystems.
-- @param #string Filename The name of the file.
-- @return #boolean outcome True if reading is possible and successful, else false.
-- @return #table data The data read from the filesystem (table of lines of text). Each line is one single #string!
function UTILS.LoadFromFile(Path,Filename)
- -- Thanks to @FunkyFranky
+ -- Thanks to @FunkyFranky
-- Check io module is available.
if not io then
BASE:E("ERROR: io not desanitized. Can't save current state.")
return false
end
-
+
-- Check default path.
if Path==nil and not lfs then
BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
end
-
+
-- Set path or default.
local path = nil
if lfs then
path=Path or lfs.writedir()
end
-
+
-- Set file name.
local filename=Filename
if path~=nil then
filename=path.."\\"..filename
end
-
+
-- Check if file exists.
local exists=UTILS.CheckFileExists(Path,Filename)
if not exists then
BASE:I(string.format("ERROR: File %s does not exist!",filename))
return false
end
-
+
-- read
local file=assert(io.open(filename, "rb"))
local loadeddata = {}
@@ -2610,30 +2622,30 @@ function UTILS.CheckFileExists(Path,Filename)
return false
end
end
-
+
-- Check io module is available.
if not io then
BASE:E("ERROR: io not desanitized.")
return false
end
-
+
-- Check default path.
if Path==nil and not lfs then
BASE:E("WARNING: lfs not desanitized. Loading will look into your DCS installation root directory rather than your \"Saved Games\\DCS\" folder.")
end
-
+
-- Set path or default.
local path = nil
if lfs then
path=Path or lfs.writedir()
end
-
+
-- Set file name.
local filename=Filename
if path~=nil then
filename=path.."\\"..filename
end
-
+
-- Check if file exists.
local exists=_fileexists(filename)
if not exists then
@@ -2670,7 +2682,7 @@ end
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the list and find the corresponding group and save the current group size (0 when dead).
--- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites).
+-- These groups are supposed to be put on the map in the ME and have *not* moved (e.g. stationary SAM sites).
-- Position is still saved for your usage.
-- The idea is to reduce the number of units when reloading the data again to restart the saved mission.
-- The data will be a simple comma separated list of groupname and size, with one header line.
@@ -2709,12 +2721,12 @@ end
-- @return #boolean outcome True if saving is successful, else false.
-- @usage
-- We will go through the set and find the corresponding group and save the current group size and current position.
--- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce
--- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups
+-- The idea is to respawn the groups **spawned during an earlier run of the mission** at the given location and reduce
+-- the number of units in the group when reloading the data again to restart the saved mission. Note that *dead* groups
-- cannot be covered with this.
-- **Note** Do NOT use dashes or hashes in group template names (-,#)!
-- The data will be a simple comma separated list of groupname and size, with one header line.
--- The current task/waypoint/etc cannot be restored.
+-- The current task/waypoint/etc cannot be restored.
function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured)
local filename = Filename or "SetOfGroups"
local data = "--Save SET of groups: "..Filename .."\n"
@@ -2724,9 +2736,12 @@ function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured)
if group and group:IsAlive() then
local name = group:GetName()
local template = string.gsub(name,"-(.+)$","")
+ if string.find(name,"AID") then
+ template = string.gsub(name,"(.AID.%d+$","")
+ end
if string.find(template,"#") then
template = string.gsub(name,"#(%d+)$","")
- end
+ end
local units = group:CountAliveUnits()
local position = group:GetVec3()
if Structured then
@@ -2738,7 +2753,7 @@ function UTILS.SaveSetOfGroups(Set,Path,Filename,Structured)
data = string.format("%s%s,%s,%d,%d,%d,%d,%s\n",data,name,template,units,position.x,position.y,position.z,strucdata)
else
data = string.format("%s%s,%s,%d,%d,%d,%d\n",data,name,template,units,position.x,position.y,position.z)
- end
+ end
end
end
-- save the data
@@ -2809,16 +2824,16 @@ end
-- @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)
-
+
local fires = {}
-
+
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)
table.insert(fires,name)
end
-
+
local function Cruncher(group,typename,anzahl)
local units = group:GetUnits()
local reduced = 0
@@ -2836,7 +2851,7 @@ function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinema
end
end
end
-
+
local reduce = true
if Reduce == false then reduce = false end
local filename = Filename or "StateListofGroups"
@@ -2878,13 +2893,13 @@ function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinema
end
local reduce = false
if loadednumber < _number then reduce = true end
-
- --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce)))
-
+
+ --BASE:I(string.format("Looking at: %s | Original number: %d | Loaded number: %d | Reduce: %s",_name,_number,loadednumber,tostring(reduce)))
+
if reduce then
- Cruncher(actualgroup,_name,_number-loadednumber)
+ Cruncher(actualgroup,_name,_number-loadednumber)
end
-
+
end
else
local reduction = actualgroup:CountAliveUnits() - size
@@ -2899,7 +2914,7 @@ function UTILS.LoadStationaryListOfGroups(Path,Filename,Reduce,Structured,Cinema
end
end
table.insert(datatable,data)
- end
+ end
else
return nil
end
@@ -2914,11 +2929,11 @@ 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.
--- @return Core.Set#SET_GROUP Set of GROUP objects.
+-- @return Core.Set#SET_GROUP Set of GROUP objects.
-- Returns nil when file cannot be read. Returns a table of data entries if Spawn is false: `{ groupname=groupname, size=size, coordinate=coordinate, template=template }`
-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,Density)
-
+
local fires = {}
local usedtemplates = {}
local spawn = true
@@ -2926,14 +2941,14 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
local filename = Filename or "SetOfGroups"
local setdata = SET_GROUP:New()
local datatable = {}
-
+
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)
table.insert(fires,name)
end
-
+
local function Cruncher(group,typename,anzahl)
local units = group:GetUnits()
local reduced = 0
@@ -2951,7 +2966,7 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
end
end
end
-
+
local function PostSpawn(args)
local spwndgrp = args[1]
local size = args[2]
@@ -2961,16 +2976,16 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
local actualsize = spwndgrp:CountAliveUnits()
if actualsize > size then
if Structured and structure then
-
+
local loadedstructure = {}
local strcset = UTILS.Split(structure,";")
for _,_data in pairs(strcset) do
local datasplit = UTILS.Split(_data,"==")
loadedstructure[datasplit[1]] = tonumber(datasplit[2])
end
-
+
local originalstructure = UTILS.GetCountPerTypeName(spwndgrp)
-
+
for _name,_number in pairs(originalstructure) do
local loadednumber = 0
if loadedstructure[_name] then
@@ -2978,11 +2993,11 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
end
local reduce = false
if loadednumber < _number then reduce = true end
-
+
if reduce then
- Cruncher(spwndgrp,_name,_number-loadednumber)
+ Cruncher(spwndgrp,_name,_number-loadednumber)
end
-
+
end
else
local reduction = actualsize-size
@@ -2995,16 +3010,16 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
end
end
end
-
+
local function MultiUse(Data)
- local template = Data.template
+ local template = Data.template
if template and usedtemplates[template] and usedtemplates[template].used and usedtemplates[template].used > 1 then
-- multispawn
if not usedtemplates[template].done then
local spwnd = 0
local spawngrp = SPAWN:New(template)
spawngrp:InitLimit(0,usedtemplates[template].used)
- for _,_entry in pairs(usedtemplates[template].data) do
+ for _,_entry in pairs(usedtemplates[template].data) do
spwnd = spwnd + 1
local sgrp=spawngrp:SpawnFromCoordinate(_entry.coordinate,spwnd)
BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure})
@@ -3016,7 +3031,7 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
return false
end
end
-
+
--BASE:I("Spawn = "..tostring(spawn))
if UTILS.CheckFileExists(Path,filename) then
local outcome,loadeddata = UTILS.LoadFromFile(Path,Filename)
@@ -3050,13 +3065,13 @@ function UTILS.LoadSetOfGroups(Path,Filename,Spawn,Structured,Cinematic,Effect,D
end
end
end
- for _id,_entry in pairs (datatable) do
+ for _id,_entry in pairs (datatable) do
if spawn and not MultiUse(_entry) and _entry.size > 0 then
local group = SPAWN:New(_entry.template)
local sgrp=group:SpawnFromCoordinate(_entry.coordinate)
BASE:ScheduleOnce(0.5,PostSpawn,{sgrp,_entry.size,_entry.structure})
end
- end
+ end
else
return nil
end
@@ -3085,7 +3100,7 @@ function UTILS.LoadSetOfStatics(Path,Filename)
if StaticObject then
datatable:AddObject(StaticObject)
end
- end
+ end
else
return nil
end
@@ -3101,7 +3116,7 @@ end
-- @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.
-- @return #table Table of data objects (tables) containing staticname, size (0=dead else 1), coordinate and the static object. Dead objects will have coordinate points `{x=0,y=0,z=0}`
--- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
+-- @return #table When using Cinematic: table of names of smoke and fire objects, so they can be extinguished with `COORDINATE.StopBigSmokeAndFire( name )`
-- Returns nil when file cannot be read.
function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,Effect,Density)
local fires = {}
@@ -3137,7 +3152,7 @@ function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,E
if Cinematic then
local effect = math.random(8)
if type(Effect) == "number" then
- effect = Effect
+ effect = Effect
end
coord:BigSmokeAndFire(effect,Density,staticname)
table.insert(fires,staticname)
@@ -3147,7 +3162,7 @@ function UTILS.LoadStationaryListOfStatics(Path,Filename,Reduce,Dead,Cinematic,E
end
end
end
- end
+ end
else
return nil
end
@@ -3251,10 +3266,10 @@ function UTILS.ToStringBRAANATO(FromGrp,ToGrp)
if aspect == "" then
BRAANATO = string.format("%s, BRA, %03d, %d miles, Angels %d, Track %s",GroupWords,bearing, rangeNM, alt, track)
else
- BRAANATO = string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords, bearing, rangeNM, alt, aspect, track)
+ BRAANATO = string.format("%s, BRAA, %03d, %d miles, Angels %d, %s, Track %s",GroupWords, bearing, rangeNM, alt, aspect, track)
end
end
- return BRAANATO
+ return BRAANATO
end
--- Check if an object is contained in a table.
@@ -3299,7 +3314,7 @@ function UTILS.IsAnyInTable(Table, Objects, Key)
end
end
end
-
+
end
return false
@@ -3315,30 +3330,30 @@ end
-- @param #table Color Color of the line in RGB, e.g. {1,0,0} for red
-- @param #number Alpha Transparency factor, between 0.1 and 1
-- @param #number LineType Line type to be used, line type: 0=No line, 1=Solid, 2=Dashed, 3=Dotted, 4=Dot dash, 5=Long dash, 6=Two dash. Default 1=Solid.
--- @param #boolean ReadOnly
+-- @param #boolean ReadOnly
function UTILS.PlotRacetrack(Coordinate, Altitude, Speed, Heading, Leg, Coalition, Color, Alpha, LineType, ReadOnly)
local fix_coordinate = Coordinate
local altitude = Altitude
local speed = Speed or 350
local heading = Heading or 270
local leg_distance = Leg or 10
-
+
local coalition = Coalition or -1
local color = Color or {1,0,0}
local alpha = Alpha or 1
local lineType = LineType or 1
-
-
+
+
speed = UTILS.IasToTas(speed, UTILS.FeetToMeters(altitude), oatcorr)
-
+
local turn_radius = 0.0211 * speed -3.01
-
+
local point_two = fix_coordinate:Translate(UTILS.NMToMeters(leg_distance), heading, true, false)
local point_three = point_two:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false)
local point_four = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius)*2, heading - 90, true, false)
local circle_center_fix_four = point_two:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false)
local circle_center_two_three = fix_coordinate:Translate(UTILS.NMToMeters(turn_radius), heading - 90, true, false)
-
+
fix_coordinate:LineToAll(point_two, coalition, color, alpha, lineType)
point_four:LineToAll(point_three, coalition, color, alpha, lineType)
@@ -4012,3 +4027,46 @@ function UTILS.ClockHeadingString(refHdg,tgtHdg)
local clockPos = math.ceil((relativeAngle % 360) / 30)
return clockPos.." o'clock"
end
+
+--- Get a NATO abbreviated MGRS text for SRS use, optionally with prosody slow tag
+-- @param #string Text The input string, e.g. "MGRS 4Q FJ 12345 67890"
+-- @param #boolean Slow Optional - add slow tags
+-- @return #string Output for (Slow) spelling in SRS TTS e.g. "MGRS;4;Quebec;Foxtrot;Juliett;1;2;3;4;5;6;7;8;niner;zero;"
+function UTILS.MGRSStringToSRSFriendly(Text,Slow)
+ local Text = string.gsub(Text,"MGRS ","")
+ Text = string.gsub(Text,"%s+","")
+ Text = string.gsub(Text,"([%a%d])","%1;") -- "0;5;1;"
+ Text = string.gsub(Text,"A","Alpha")
+ Text = string.gsub(Text,"B","Bravo")
+ Text = string.gsub(Text,"C","Charlie")
+ Text = string.gsub(Text,"D","Delta")
+ Text = string.gsub(Text,"E","Echo")
+ Text = string.gsub(Text,"F","Foxtrot")
+ Text = string.gsub(Text,"G","Golf")
+ Text = string.gsub(Text,"H","Hotel")
+ Text = string.gsub(Text,"I","India")
+ Text = string.gsub(Text,"J","Juliett")
+ Text = string.gsub(Text,"K","Kilo")
+ Text = string.gsub(Text,"L","Lima")
+ Text = string.gsub(Text,"M","Mike")
+ Text = string.gsub(Text,"N","November")
+ Text = string.gsub(Text,"O","Oscar")
+ Text = string.gsub(Text,"P","Papa")
+ Text = string.gsub(Text,"Q","Quebec")
+ Text = string.gsub(Text,"R","Romeo")
+ Text = string.gsub(Text,"S","Sierra")
+ Text = string.gsub(Text,"T","Tango")
+ Text = string.gsub(Text,"U","Uniform")
+ Text = string.gsub(Text,"V","Victor")
+ Text = string.gsub(Text,"W","Whiskey")
+ Text = string.gsub(Text,"X","Xray")
+ Text = string.gsub(Text,"Y","Yankee")
+ Text = string.gsub(Text,"Z","Zulu")
+ Text = string.gsub(Text,"0","zero")
+ Text = string.gsub(Text,"9","niner")
+ if Slow then
+ Text = ''..Text..''
+ end
+ Text = "MGRS;"..Text
+ return Text
+end
diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua
index cc7330c3e..4a9c7e0f1 100644
--- a/Moose Development/Moose/Wrapper/Airbase.lua
+++ b/Moose Development/Moose/Wrapper/Airbase.lua
@@ -722,6 +722,39 @@ AIRBASE.Sinai = {
["Wadi_al_Jandali"] = "Wadi al Jandali",
}
+--- Airbases of the Kola map
+--
+-- * AIRBASE.Kola.Banak
+-- * AIRBASE.Kola.Bas_100
+-- * AIRBASE.Kola.Bodo
+-- * AIRBASE.Kola.Jokkmokk
+-- * AIRBASE.Kola.Kalixfors
+-- * AIRBASE.Kola.Kemi_Tornio
+-- * AIRBASE.Kola.Kiruna
+-- * AIRBASE.Kola.Monchegorsk
+-- * AIRBASE.Kola.Murmansk_International
+-- * AIRBASE.Kola.Olenya
+-- * AIRBASE.Kola.Rovaniemi
+-- * AIRBASE.Kola.Severomorsk_1
+-- * AIRBASE.Kola.Severomorsk_3
+--
+-- @field Kola
+AIRBASE.Kola = {
+ ["Banak"] = "Banak",
+ ["Bas_100"] = "Bas 100",
+ ["Bodo"] = "Bodo",
+ ["Jokkmokk"] = "Jokkmokk",
+ ["Kalixfors"] = "Kalixfors",
+ ["Kemi_Tornio"] = "Kemi Tornio",
+ ["Kiruna"] = "Kiruna",
+ ["Monchegorsk"] = "Monchegorsk",
+ ["Murmansk_International"] = "Murmansk International",
+ ["Olenya"] = "Olenya",
+ ["Rovaniemi"] = "Rovaniemi",
+ ["Severomorsk_1"] = "Severomorsk-1",
+ ["Severomorsk_3"] = "Severomorsk-3",
+}
+
--- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy".
-- @type AIRBASE.ParkingSpot
-- @field Core.Point#COORDINATE Coordinate Coordinate of the parking spot.
diff --git a/Moose Development/Moose/Wrapper/Group.lua b/Moose Development/Moose/Wrapper/Group.lua
index 5277eaade..e458b27d0 100644
--- a/Moose Development/Moose/Wrapper/Group.lua
+++ b/Moose Development/Moose/Wrapper/Group.lua
@@ -367,7 +367,7 @@ function GROUP:GetDCSObject()
return DCSGroup
end
- self:E(string.format("ERROR: Could not get DCS group object of group %s because DCS object could not be found!", tostring(self.GroupName)))
+ self:T2(string.format("ERROR: Could not get DCS group object of group %s because DCS object could not be found!", tostring(self.GroupName)))
return nil
end
@@ -1228,15 +1228,17 @@ function GROUP:GetCoordinate()
-- no luck, try the API way
local DCSGroup = Group.getByName(self.GroupName)
- local DCSUnits = DCSGroup:getUnits() or {}
- for _,_unit in pairs(DCSUnits) do
- if Object.isExist(_unit) then
- local position = _unit:getPosition()
- local point = position.p ~= nil and position.p or _unit:GetPoint()
- if point then
- --self:I(point)
- local coord = COORDINATE:NewFromVec3(point)
- return coord
+ if DCSGroup then
+ local DCSUnits = DCSGroup:getUnits() or {}
+ for _,_unit in pairs(DCSUnits) do
+ if Object.isExist(_unit) then
+ local position = _unit:getPosition()
+ local point = position.p ~= nil and position.p or _unit:GetPoint()
+ if point then
+ --self:I(point)
+ local coord = COORDINATE:NewFromVec3(point)
+ return coord
+ end
end
end
end
@@ -1794,10 +1796,14 @@ end
--- Returns the group template from the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
-- @param #GROUP self
--- @return #table
+-- @return #table Template table.
function GROUP:GetTemplate()
local GroupName = self:GetName()
- return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) )
+ local template=_DATABASE:GetGroupTemplate( GroupName )
+ if template then
+ return UTILS.DeepCopy( template )
+ end
+ return nil
end
--- Returns the group template route.points[] (the waypoints) from the global _DATABASE object (an instance of @{Core.Database#DATABASE}).
diff --git a/Moose Development/Moose/Wrapper/Weapon.lua b/Moose Development/Moose/Wrapper/Weapon.lua
index 5c9ebc53d..3e75d8672 100644
--- a/Moose Development/Moose/Wrapper/Weapon.lua
+++ b/Moose Development/Moose/Wrapper/Weapon.lua
@@ -40,6 +40,7 @@
-- @field #number coalition Coalition ID.
-- @field #number country Country ID.
-- @field DCS#Desc desc Descriptor table.
+-- @field DCS#Desc guidance Missile guidance descriptor.
-- @field DCS#Unit launcher Launcher DCS unit.
-- @field Wrapper.Unit#UNIT launcherUnit Launcher Unit.
-- @field #string launcherName Name of launcher unit.
@@ -196,6 +197,9 @@ function WEAPON:New(WeaponObject)
if self:IsMissile() and self.desc.missileCategory then
self.categoryMissile=self.desc.missileCategory
+ if self.desc.guidance then
+ self.guidance = self.desc.guidance
+ end
end
-- Get type name.
@@ -667,6 +671,26 @@ function WEAPON:IsTorpedo()
return self.category==Weapon.Category.TORPEDO
end
+--- Check if weapon is a Fox One missile (Radar Semi-Active).
+-- @param #WEAPON self
+-- @return #boolean If `true`, is a Fox One.
+function WEAPON:IsFoxOne()
+ return self.guidance==Weapon.GuidanceType.RADAR_SEMI_ACTIVE
+end
+
+--- Check if weapon is a Fox Two missile (IR guided).
+-- @param #WEAPON self
+-- @return #boolean If `true`, is a Fox Two.
+function WEAPON:IsFoxTwo()
+ return self.guidance==Weapon.GuidanceType.IR
+end
+
+--- Check if weapon is a Fox Three missile (Radar Active).
+-- @param #WEAPON self
+-- @return #boolean If `true`, is a Fox Three.
+function WEAPON:IsFoxThree()
+ return self.guidance==Weapon.GuidanceType.RADAR_ACTIVE
+end
--- Destroy the weapon object.
-- @param #WEAPON self