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