diff --git a/Moose Development/Moose/Core/ClientMenu.lua b/Moose Development/Moose/Core/ClientMenu.lua new file mode 100644 index 000000000..d58c62f4b --- /dev/null +++ b/Moose Development/Moose/Core/ClientMenu.lua @@ -0,0 +1,785 @@ +--- **Core** - Client Menu Management. +-- +-- **Main Features:** +-- +-- * For complex, non-static menu structures +-- * Lightweigt implementation as alternative to MENU +-- * Separation of menu tree creation from menu on the clients's side +-- * Works with a SET_CLIENT set of clients +-- * Allow manipulation of the shadow tree in various ways +-- * Push to all or only one client +-- * Change entries' menu text +-- * Option to make an entry usable once only across all clients +-- * Auto appends GROUP and CLIENT objects to menu calls +-- +-- === +-- +-- ### Author: **applevangelist** +-- +-- === +-- +-- @module Core.ClientMenu +-- @image Core_Menu.JPG +-- last change: July 2023 + +-- TODO +---------------------------------------------------------------------------------------------------------------- +-- +-- CLIENTMENU +-- +---------------------------------------------------------------------------------------------------------------- + +--- +-- @type CLIENTMENU +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string name Name +-- @field #table path +-- @field #table parentpath +-- @field #CLIENTMENU Parent +-- @field Wrapper.Client#CLIENT client +-- @field #number GroupID Group ID +-- @field #number ID Entry ID +-- @field Wrapper.Group#GROUP group +-- @field #string UUID Unique ID based on path+name +-- @field #string Function +-- @field #table Functionargs +-- @field #table Children +-- @field #boolean Once +-- @field #boolean Generic +-- @field #boolean debug +-- @field #CLIENTMENUMANAGER Controller +-- @extends Core.Base#BASE + +--- +-- @field #CLIENTMENU +CLIENTMENU = { + ClassName = "CLIENTMENUE", + lid = "", + version = "0.1.0", + name = nil, + path = nil, + group = nil, + client = nil, + GroupID = nil, + Children = {}, + Once = false, + Generic = false, + debug = false, + Controller = nil, +} + +--- +-- @field #CLIENTMENU_ID +CLIENTMENU_ID = 0 + +--- Create an new CLIENTMENU object. +-- @param #CLIENTMENU self +-- @param Wrapper.Client#CLIENT Client The client for whom this entry is. +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated +-- @return #CLIENTMENU self +function CLIENTMENU:NewEntry(Client,Text,Parent,Function,...) + -- Inherit everything from BASE class. + local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENU + CLIENTMENU_ID = CLIENTMENU_ID + 1 + self.ID = CLIENTMENU_ID + if Client then + self.group = Client:GetGroup() + self.client = Client + self.GroupID = self.group:GetID() + else + self.Generic = true + end + self.name = Text or "unknown entry" + if Parent then + if Parent:IsInstanceOf("MENU_BASE") then + self.parentpath = Parent.MenuPath + else + self.parentpath = Parent:GetPath() + Parent:AddChild(self) + end + end + self.Parent = Parent + self.Function = Function + self.Functionargs = arg or {} + table.insert(self.Functionargs,self.group) + table.insert(self.Functionargs,self.client) + if self.Functionargs and self.debug then + self:T({"Functionargs",self.Functionargs}) + end + if not self.Generic then + if Function ~= nil then + local ErrorHandler = function( errmsg ) + env.info( "MOOSE Error in CLIENTMENU COMMAND function: " .. errmsg ) + if BASE.Debug ~= nil then + env.info( BASE.Debug.traceback() ) + end + return errmsg + end + self.CallHandler = function() + local function MenuFunction() + return self.Function( unpack( self.Functionargs ) ) + end + local Status, Result = xpcall( MenuFunction, ErrorHandler) + if self.Once == true then + self:Clear() + end + end + self.path = missionCommands.addCommandForGroup(self.GroupID,Text,self.parentpath, self.CallHandler) + else + self.path = missionCommands.addSubMenuForGroup(self.GroupID,Text,self.parentpath) + end + else + if self.parentpath then + self.path = UTILS.DeepCopy(self.parentpath) + else + self.path = {} + end + self.path[#self.path+1] = Text + end + self.UUID = table.concat(self.path,";") + self:T({self.UUID}) + self.Once = false + -- Log id. + self.lid=string.format("CLIENTMENU %s | %s | ", self.ID, self.name) + self:T(self.lid.."Created") + return self +end + +--- Create a UUID +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Parent The parent object if any +-- @param #string Text The menu entry text +-- @return #string UUID +function CLIENTMENU:CreateUUID(Parent,Text) + local path = {} + if Parent and Parent.path then + path = Parent.path + end + path[#path+1] = Text + local UUID = table.concat(path,";") + return UUID +end + +--- Set the CLIENTMENUMANAGER for this entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENUMANAGER Controller The controlling object. +-- @return #CLIENTMENU self +function CLIENTMENU:SetController(Controller) + self.Controller = Controller + return self +end + +--- The entry will be deleted after being used used - for menu entries with functions only. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:SetOnce() + self:T(self.lid.."SetOnce") + self.Once = true + return self +end + +--- Remove the entry from the F10 menu. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveF10() + self:T(self.lid.."RemoveF10") + if self.GroupID then + --self:I(self.lid.."Removing "..table.concat(self.path,";")) + missionCommands.removeItemForGroup(self.GroupID , self.path ) + end + return self +end + +--- Get the menu path table. +-- @param #CLIENTMENU self +-- @return #table Path +function CLIENTMENU:GetPath() + self:T(self.lid.."GetPath") + return self.path +end + +--- Get the UUID. +-- @param #CLIENTMENU self +-- @return #string UUID +function CLIENTMENU:GetUUID() + self:T(self.lid.."GetUUID") + return self.UUID +end + +--- Link a child entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Child The entry to link as a child. +-- @return #CLIENTMENU self +function CLIENTMENU:AddChild(Child) + self:T(self.lid.."AddChild "..Child.ID) + table.insert(self.Children,Child.ID,Child) + return self +end + +--- Remove a child entry. +-- @param #CLIENTMENU self +-- @param #CLIENTMENU Child The entry to remove from the children. +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveChild(Child) + self:T(self.lid.."RemoveChild "..Child.ID) + table.remove(self.Children,Child.ID) + return self +end + +--- Remove all subentries (children) from this entry. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:RemoveSubEntries() + self:T(self.lid.."RemoveSubEntries") + self:T({self.Children}) + for _id,_entry in pairs(self.Children) do + self:T("Removing ".._id) + if _entry then + _entry:RemoveSubEntries() + _entry:RemoveF10() + if _entry.Parent then + _entry.Parent:RemoveChild(self) + end + --if self.Controller then + --self.Controller:_RemoveByID(_entry.ID) + --end + --_entry = nil + end + end + return self +end + +--- Remove this entry and all subentries (children) from this entry. +-- @param #CLIENTMENU self +-- @return #CLIENTMENU self +function CLIENTMENU:Clear() + self:T(self.lid.."Clear") + for _id,_entry in pairs(self.Children) do + if _entry then + _entry:RemoveSubEntries() + _entry = nil + end + end + self:RemoveF10() + if self.Parent then + self.Parent:RemoveChild(self) + end + --if self.Controller then + --self.Controller:_RemoveByID(self.ID) + --end + return self +end + +-- TODO +---------------------------------------------------------------------------------------------------------------- +-- +-- CLIENTMENUMANAGER +-- +---------------------------------------------------------------------------------------------------------------- + + +--- Class CLIENTMENUMANAGER +-- @type CLIENTMENUMANAGER +-- @field #string ClassName Class Name +-- @field #string lid Lid for log entries +-- @field #string version Version string +-- @field #string name Name +-- @field Core.Set#SET_CLIENT clientset The set of clients this menu manager is for +-- @field #table flattree +-- @field #table rootentries +-- @field #table menutree +-- @field #number entrycount +-- @field #boolean debug +-- @extends Core.Base#BASE + +--- *As a child my family's menu consisted of two choices: take it, or leave it.* +-- +-- === +-- +-- ## CLIENTMENU and CLIENTMENUMANAGER +-- +-- Manage menu structures for a SET_CLIENT of clients. +-- +-- ## Concept +-- +-- Separate creation of a menu tree structure from pushing it to each client. Create a shadow "reference" menu structure tree for your client pilot's in a mission. +-- This can then be propagated to all clients. Manipulate the entries in the structure with removing, clearing or changing single entries, create replacement sub-structures +-- for entries etc, push to one or all clients. +-- +-- Many functions can either change the tree for one client or for all clients. +-- +-- ## Create a base reference tree and send to all clients +-- +-- local clientset = SET_CLIENT:New():FilterStart() +-- +-- local menumgr = CLIENTMENUMANAGER:New(clientset,"Dayshift") +-- local mymenu = menumgr:NewEntry("Top") +-- local mymenu_lv1a = menumgr:NewEntry("Level 1 a",mymenu) +-- local mymenu_lv1b = menumgr:NewEntry("Level 1 b",mymenu) +-- -- next one is a command menu entry, which can only be used once +-- local mymenu_lv1c = menumgr:NewEntry("Action Level 1 c",mymenu, testfunction, "testtext"):SetOnce() +-- +-- local mymenu_lv2a = menumgr:NewEntry("Go here",mymenu_lv1a) +-- local mymenu_lv2b = menumgr:NewEntry("Level 2 ab",mymenu_lv1a) +-- local mymenu_lv2c = menumgr:NewEntry("Level 2 ac",mymenu_lv1a) +-- +-- local mymenu_lv2ba = menumgr:NewEntry("Level 2 ba",mymenu_lv1b) +-- local mymenu_lv2bb = menumgr:NewEntry("Level 2 bb",mymenu_lv1b) +-- local mymenu_lv2bc = menumgr:NewEntry("Level 2 bc",mymenu_lv1b) +-- +-- local mymenu_lv3a = menumgr:NewEntry("Level 3 aaa",mymenu_lv2a) +-- local mymenu_lv3b = menumgr:NewEntry("Level 3 aab",mymenu_lv2a) +-- local mymenu_lv3c = menumgr:NewEntry("Level 3 aac",mymenu_lv2a) +-- +-- menumgr:Propagate() +-- +-- ## Remove a single entry's subtree +-- +-- menumgr:RemoveSubEntries(mymenu_lv3a) +-- + -- ## Remove a single entry and also it's subtree +-- +-- menumgr:DeleteEntry(mymenu_lv3a) +-- +-- ## Add a single entry +-- +-- local baimenu = menumgr:NewEntry("BAI",mymenu_lv1b) +-- +-- menumgr:AddEntry(baimenu) +-- +-- ## Add an entry with a function +-- +-- local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, Argument1, Argument1) +-- +-- Now, the class will **automatically append the call with GROUP and CLIENT objects**, as this is can only be done when pushing the entry to the clients. So, the actual function implementation needs to look like this: +-- +-- function TestFunction( Argument1, Argument2, Group, Client) +-- +-- **Caveat is**, that you need to ensure your arguments are not **nil** or **false**, as LUA will optimize those away. You would end up having Group and Client in wrong places in the function call. Hence, +-- if you need/ want to send **nil** or **false**, send a place holder instead and ensure your function can handle this, e.g. +-- +-- local baimenu = menumgr:NewEntry("Task Action", mymenu_lv1b, TestFunction, "nil", Argument1) +-- +-- ## Change the text of a leaf entry in the menu tree +-- +-- menumgr:ChangeEntryTextForAll(mymenu_lv1b,"Attack") +-- +-- ## Reset a single clients menu tree +-- +-- menumgr:ResetMenu(client) +-- +-- ## Reset all and clear the reference tree +-- +-- menumgr:ResetMenuComplete() +-- +-- @field #CLIENTMENUMANAGER +CLIENTMENUMANAGER = { + ClassName = "CLIENTMENUMANAGER", + lid = "", + version = "0.1.0", + name = nil, + clientset = nil, + menutree = {}, + flattree = {}, + playertree = {}, + entrycount = 0, + rootentries = {}, + debug = true, +} + +--- Create a new ClientManager instance. +-- @param #CLIENTMENUMANAGER self +-- @param Core.Set#SET_CLIENT ClientSet The set of clients to manage. +-- @param #string Alias The name of this manager. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:New(ClientSet, Alias) + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, BASE:New()) -- #CLIENTMENUMANAGER + self.clientset = ClientSet + self.name = Alias or "Nightshift" + -- Log id. + self.lid=string.format("CLIENTMENUMANAGER %s | %s | ", self.version, self.name) + if self.debug then + self:T(self.lid.."Created") + end + return self +end + +--- Create a new entry in the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text of the F10 menu entry. +-- @param #CLIENTMENU Parent The parent menu entry. +-- @param #string Function (optional) Function to call when the entry is used. +-- @param ... (optional) Arguments for the Function, comma separated. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:NewEntry(Text,Parent,Function,...) + self:T(self.lid.."NewEntry "..Text or "None") + self.entrycount = self.entrycount + 1 + local entry = CLIENTMENU:NewEntry(nil,Text,Parent,Function,unpack(arg)) + if not Parent then + self.rootentries[self.entrycount] = entry + end + local depth = #entry.path + if not self.menutree[depth] then self.menutree[depth] = {} end + table.insert(self.menutree[depth],entry.UUID) + self.flattree[entry.UUID] = entry + return entry +end + +--- Check matching entry in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string UUID UUID of the menu entry. +-- @return #boolean Exists +function CLIENTMENUMANAGER:EntryUUIDExists(UUID) + local exists = self.flattree[UUID] and true or false + return exists +end + +--- Find matching entry in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string UUID UUID of the menu entry. +-- @return #CLIENTMENU Entry The #CLIENTMENU object found or nil. +function CLIENTMENUMANAGER:FindEntryByUUID(UUID) + self:T(self.lid.."FindEntryByUUID "..UUID or "None") + local entry = nil + for _gid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Entry and Entry.UUID == UUID then + entry = Entry + end + end + return entry +end + +--- Find matching entries by text in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text or partial text of the menu entry to find. +-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry. +-- @return #table Table of matching UUIDs of #CLIENTMENU objects +-- @return #table Table of matching #CLIENTMENU objects +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindUUIDsByText(Text,Parent) + self:T(self.lid.."FindUUIDsByText "..Text or "None") + local matches = {} + local entries = {} + local n = 0 + for _uuid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Parent then + if Entry and string.find(Entry.name,Text) and string.find(Entry.UUID,Parent.UUID) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + else + if Entry and string.find(Entry.name,Text) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + end + end + return matches, entries, n +end + +--- Find matching entries in the generic structure by the menu text. +-- @param #CLIENTMENUMANAGER self +-- @param #string Text Text or partial text of the F10 menu entry. +-- @param #CLIENTMENU Parent (Optional) Only find entries under this parent entry. +-- @return #table Table of matching #CLIENTMENU objects. +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindEntriesByText(Text,Parent) + self:T(self.lid.."FindEntriesByText "..Text or "None") + local matches, objects, number = self:FindUUIDsByText(Text, Parent) + return objects, number +end + +--- Find matching entries under a parent in the generic structure by UUID. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent Find entries under this parent entry. +-- @return #table Table of matching UUIDs of #CLIENTMENU objects +-- @return #table Table of matching #CLIENTMENU objects +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindUUIDsByParent(Parent) + self:T(self.lid.."FindUUIDsByParent") + local matches = {} + local entries = {} + local n = 0 + for _uuid,_entry in pairs(self.flattree) do + local Entry = _entry -- #CLIENTMENU + if Parent then + if Entry and string.find(Entry.UUID,Parent.UUID) then + table.insert(matches,_uuid) + table.insert(entries,Entry ) + n=n+1 + end + end + end + return matches, entries, n +end + +--- Find matching entries in the generic structure under a parent. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Parent Find entries under this parent entry. +-- @return #table Table of matching #CLIENTMENU objects. +-- @return #number Number of matches +function CLIENTMENUMANAGER:FindEntriesByParent(Parent) + self:T(self.lid.."FindEntriesByParent") + local matches, objects, number = self:FindUUIDsByParent(Parent) + return objects, number +end + +--- Alter the text of a leaf entry in the generic structure and push to one specific client's F10 menu. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The menu entry. +-- @param #string Text New Text of the F10 menu entry. +-- @param Wrapper.Client#CLIENT Client (optional) The client for whom to alter the entry, if nil done for all clients. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ChangeEntryText(Entry, Text, Client) + self:T(self.lid.."ChangeEntryText "..Text or "None") + local newentry = CLIENTMENU:NewEntry(nil,Text,Entry.Parent,Entry.Function,unpack(Entry.Functionargs)) + self:DeleteF10Entry(Entry,Client) + self:DeleteGenericEntry(Entry) + if not Entry.Parent then + self.rootentries[self.entrycount] = newentry + end + local depth = #newentry.path + if not self.menutree[depth] then self.menutree[depth] = {} end + table.insert(self.menutree[depth],newentry.UUID) + self.flattree[newentry.UUID] = newentry + self:AddEntry(newentry,Client) + return self +end + +--- Push the complete menu structure to each of the clients in the set - refresh the menu tree of the clients. +-- @param #CLIENTMENUMANAGER self +-- @param Wrapper.Client#CLIENT Client (optional) If given, propagate only for this client. +-- @return #CLIENTMENU Entry +function CLIENTMENUMANAGER:Propagate(Client) + self:T(self.lid.."Propagate") + self:T(Client) + local Set = self.clientset.Set + if Client then + Set = {Client} + end + self:ResetMenu(Client) + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + if not self.playertree[playername] then + self.playertree[playername] = {} + end + for level,branch in pairs (self.menutree) do + self:T("Building branch:" .. level) + for _,leaf in pairs(branch) do + self:T("Building leaf:" .. leaf) + local entry = self:FindEntryByUUID(leaf) + if entry then + self:T("Found generic entry:" .. entry.UUID) + local parent = nil + if entry.Parent and entry.Parent.UUID then + parent = self.playertree[playername][entry.Parent.UUID] or self:FindEntryByUUID(entry.Parent.UUID) + end + self.playertree[playername][entry.UUID] = CLIENTMENU:NewEntry(client,entry.name,parent,entry.Function,unpack(entry.Functionargs)) + self.playertree[playername][entry.UUID].Once = entry.Once + else + self:T("NO generic entry for:" .. leaf) + end + end + end + end + end + return self +end + +--- Push a single previously created entry into the menu structure of all clients. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to add. +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:AddEntry(Entry,Client) + self:T(self.lid.."AddEntry") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + local client = _client -- Wrapper.Client#CLIENT + if client and client:IsAlive() then + local playername = client:GetPlayerName() + if Entry then + self:T("Adding generic entry:" .. Entry.UUID) + local parent = nil + if not self.playertree[playername] then + self.playertree[playername] = {} + end + if Entry.Parent and Entry.Parent.UUID then + parent = self.playertree[playername][Entry.Parent.UUID] or self:FindEntryByUUID(Entry.Parent.UUID) + end + self.playertree[playername][Entry.UUID] = CLIENTMENU:NewEntry(client,Entry.name,parent,Entry.Function,unpack(Entry.Functionargs)) + self.playertree[playername][Entry.UUID].Once = Entry.Once + else + self:T("NO generic entry given") + end + end + end + return self +end + +--- Blank out the menu - remove **all root entries** and all entries below from the client's F10 menus, leaving the generic structure untouched. +-- @param #CLIENTMENUMANAGER self +-- @param Wrapper.Client#CLIENT Client (optional) If given, remove only for this client. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ResetMenu(Client) + self:T(self.lid.."ResetMenu") + for _,_entry in pairs(self.rootentries) do + --local RootEntry = self.structure.generic[_entry] + if _entry then + self:DeleteF10Entry(_entry,Client) + end + end + return self +end + +--- Blank out the menu - remove **all root entries** and all entries below from all clients' F10 menus, and **delete** the generic structure. +-- @param #CLIENTMENUMANAGER self +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:ResetMenuComplete() + self:T(self.lid.."ResetMenuComplete") + for _,_entry in pairs(self.rootentries) do + --local RootEntry = self.structure.generic[_entry] + if _entry then + self:DeleteF10Entry(_entry) + end + end + self.playertree = nil + self.playertree = {} + self.rootentries = nil + self.rootentries = {} + self.menutree = nil + self.menutree = {} + return self +end + +--- Remove the entry and all entries below the given entry from the client's F10 menus. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:DeleteF10Entry(Entry,Client) + self:T(self.lid.."DeleteF10Entry") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + if _client and _client:IsAlive() then + local playername = _client:GetPlayerName() + if self.playertree[playername] then + local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU + if centry then + --self:I("Match for "..Entry.UUID) + centry:Clear() + end + end + end + end + return self +end + +--- Remove the entry and all entries below the given entry from the generic tree. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry to remove +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:DeleteGenericEntry(Entry) + self:T(self.lid.."DeleteGenericEntry") + + if Entry.Children and #Entry.Children > 0 then + self:RemoveGenericSubEntries(Entry) + end + + local depth = #Entry.path + local uuid = Entry.UUID + + local tbl = UTILS.DeepCopy(self.menutree) + + if tbl[depth] then + for i=depth,#tbl do + --self:I("Level = "..i) + for _id,_uuid in pairs(tbl[i]) do + self:T(_uuid) + if string.find(_uuid,uuid) or _uuid == uuid then + --self:I("Match for ".._uuid) + self.menutree[i][_id] = nil + self.flattree[_uuid] = nil + end + end + end + end + + return self +end + +--- Remove all entries below the given entry from the generic tree. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry where to start. This entry stays. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:RemoveGenericSubEntries(Entry) + self:T(self.lid.."RemoveGenericSubEntries") + + local depth = #Entry.path + 1 + local uuid = Entry.UUID + + local tbl = UTILS.DeepCopy(self.menutree) + + if tbl[depth] then + for i=depth,#tbl do + self:T("Level = "..i) + for _id,_uuid in pairs(tbl[i]) do + self:T(_uuid) + if string.find(_uuid,uuid) then + self:T("Match for ".._uuid) + self.menutree[i][_id] = nil + self.flattree[_uuid] = nil + end + end + end + end + return self +end + + +--- Remove all entries below the given entry from the client's F10 menus. +-- @param #CLIENTMENUMANAGER self +-- @param #CLIENTMENU Entry The entry where to start. This entry stays. +-- @param Wrapper.Client#CLIENT Client (optional) If given, make this change only for this client. In this case the generic structure will not be touched. +-- @return #CLIENTMENUMANAGER self +function CLIENTMENUMANAGER:RemoveF10SubEntries(Entry,Client) + self:T(self.lid.."RemoveSubEntries") + local Set = self.clientset.Set + if Client then + Set = {Client} + end + for _,_client in pairs(Set) do + if _client and _client:IsAlive() then + local playername = _client:GetPlayerName() + if self.playertree[playername] then + local centry = self.playertree[playername][Entry.UUID] -- #CLIENTMENU + centry:RemoveSubEntries() + end + end + end + return self +end + +---------------------------------------------------------------------------------------------------------------- +-- +-- End ClientMenu +-- +---------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index d942e49ba..e699af694 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2944,8 +2944,13 @@ do -- COORDINATE if alt < 1 then alttext = "very low" end - - local track = UTILS.BearingToCardinal(bearing) or "North" + + -- corrected Track to be direction of travel of bogey (self in this case) + local track = "Maneuver" + + if self.Heading then + track = UTILS.BearingToCardinal(self.Heading) or "North" + end if rangeNM > 3 then if SSML then -- google says "oh" instead of zero, be aware diff --git a/Moose Development/Moose/Core/Spawn.lua b/Moose Development/Moose/Core/Spawn.lua index df22111c3..95719aa9e 100644 --- a/Moose Development/Moose/Core/Spawn.lua +++ b/Moose Development/Moose/Core/Spawn.lua @@ -162,7 +162,17 @@ -- ### Array formation -- -- * @{#SPAWN.InitArray}(): Make groups visible before they are actually activated, and order these groups like a battalion in an array. --- +-- +-- ### Group initial position - if wanted different from template position, for use with e.g. @{#SPAWN.SpawnScheduled}(). +-- +-- * @{#SPAWN.InitPositionCoordinate}(): Set initial position of group via a COORDINATE. +-- * @{#SPAWN.InitPositionVec2}(): Set initial position of group via a VEC2. +-- +-- ### Set the positions of a group's units to absolute positions, or relative positions to unit No. 1 +-- +-- * @{#SPAWN.InitSetUnitRelativePositions}(): Spawn the UNITs of this group with individual relative positions to unit #1 and individual headings. +-- * @{#SPAWN.InitSetUnitAbsolutePositions}(): Spawn the UNITs of this group with individual absolute positions and individual headings. +-- -- ### Position randomization -- -- * @{#SPAWN.InitRandomizePosition}(): Randomizes the position of @{Wrapper.Group}s that are spawned within a **radius band**, given an Outer and Inner radius, from the point that the spawn happens. @@ -268,7 +278,7 @@ SPAWN = { -- @type SPAWN.Takeoff -- @extends Wrapper.Group#GROUP.Takeoff ---- @field #SPAWN.Takeoff Takeoff +-- @field #SPAWN.Takeoff Takeoff SPAWN.Takeoff = { Air = 1, Runway = 2, @@ -276,7 +286,7 @@ SPAWN.Takeoff = { Cold = 4, } ---- @type SPAWN.SpawnZoneTable +-- @type SPAWN.SpawnZoneTable -- @list SpawnZone --- Creates the main object to spawn a @{Wrapper.Group} defined in the DCS ME. @@ -1047,7 +1057,7 @@ end --- This method provides the functionality to randomize the spawning of the Groups at a given list of zones of different types. -- @param #SPAWN self -- @param #table SpawnZoneTable A table with @{Core.Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Core.Zone}s objects. --- @return #SPAWN +-- @return #SPAWN self -- @usage -- -- -- Create a zone table of the 2 zones. @@ -1077,6 +1087,31 @@ function SPAWN:InitRandomizeZones( SpawnZoneTable ) return self end +--- This method sets a spawn position for the group that is different from the location of the template. +-- @param #SPAWN self +-- @param Core.Point#COORDINATE Coordinate The position to spawn from +-- @return #SPAWN self +function SPAWN:InitPositionCoordinate(Coordinate) + self:T( { self.SpawnTemplatePrefix, Coordinate:GetVec2()} ) + self:InitPositionVec2(Coordinate:GetVec2()) + return self +end + +--- This method sets a spawn position for the group that is different from the location of the template. +-- @param #SPAWN self +-- @param DCS#Vec2 Vec2 The position to spawn from +-- @return #SPAWN self +function SPAWN:InitPositionVec2(Vec2) + self:T( { self.SpawnTemplatePrefix, Vec2} ) + self.SpawnInitPosition = Vec2 + self.SpawnFromNewPosition = true + self:I("MaxGroups:"..self.SpawnMaxGroups) + for SpawnGroupID = 1, self.SpawnMaxGroups do + self:_SetInitialPosition( SpawnGroupID ) + end + return self +end + --- For planes and helicopters, when these groups go home and land on their home airbases and FARPs, they normally would taxi to the parking spot, shut-down their engines and wait forever until the Group is removed by the runtime environment. -- This method is used to re-spawn automatically (so no extra call is needed anymore) the same group after it has landed. -- This will enable a spawned group to be re-spawned after it lands, until it is destroyed... @@ -1376,7 +1411,12 @@ function SPAWN:SpawnWithIndex( SpawnIndex, NoBirth ) self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } ) if self:_GetSpawnIndex( SpawnIndex ) then - + + if self.SpawnFromNewPosition then + self:_SetInitialPosition( SpawnIndex ) + end + + if self.SpawnGroups[self.SpawnIndex].Visible then self.SpawnGroups[self.SpawnIndex].Group:Activate() else @@ -1614,7 +1654,7 @@ end -- @param #number SpawnTime The time interval defined in seconds between each new spawn of new groups. -- @param #number SpawnTimeVariation The variation to be applied on the defined time interval between each new spawn. -- The variation is a number between 0 and 1, representing the % of variation to be applied on the time interval. --- @param #boolen WithDelay Do not spawn the **first** group immediately, but delay the spawn as per the calculation below. +-- @param #boolean WithDelay Do not spawn the **first** group immediately, but delay the spawn as per the calculation below. -- Effectively the same as @{InitDelayOn}(). -- @return #SPAWN self -- @usage @@ -3127,7 +3167,11 @@ function SPAWN:_GetTemplate( SpawnTemplatePrefix ) self:F( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnTemplatePrefix } ) local SpawnTemplate = nil - + + if _DATABASE.Templates.Groups[SpawnTemplatePrefix] == nil then + error( 'No Template exists for SpawnTemplatePrefix = ' .. SpawnTemplatePrefix ) + end + local Template = _DATABASE.Templates.Groups[SpawnTemplatePrefix].Template self:F( { Template = Template } ) @@ -3296,6 +3340,57 @@ function SPAWN:_RandomizeTemplate( SpawnIndex ) return self end +--- Private method that sets the DCS#Vec2 where the Group will be spawned. +-- @param #SPAWN self +-- @param #number SpawnIndex +-- @return #SPAWN self +function SPAWN:_SetInitialPosition( SpawnIndex ) + self:T( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } ) + + if self.SpawnFromNewPosition then + + self:T( "Preparing Spawn at Vec2 ", self.SpawnInitPosition ) + + local SpawnVec2 = self.SpawnInitPosition + + self:T( { SpawnVec2 = SpawnVec2 } ) + + local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate + + SpawnTemplate.route = SpawnTemplate.route or {} + SpawnTemplate.route.points = SpawnTemplate.route.points or {} + SpawnTemplate.route.points[1] = SpawnTemplate.route.points[1] or {} + SpawnTemplate.route.points[1].x = SpawnTemplate.route.points[1].x or 0 + SpawnTemplate.route.points[1].y = SpawnTemplate.route.points[1].y or 0 + + self:T( { Route = SpawnTemplate.route } ) + + for UnitID = 1, #SpawnTemplate.units do + local UnitTemplate = SpawnTemplate.units[UnitID] + self:T( 'Before Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y ) + local SX = UnitTemplate.x + local SY = UnitTemplate.y + local BX = SpawnTemplate.route.points[1].x + local BY = SpawnTemplate.route.points[1].y + local TX = SpawnVec2.x + (SX - BX) + local TY = SpawnVec2.y + (SY - BY) + UnitTemplate.x = TX + UnitTemplate.y = TY + -- TODO: Manage altitude based on landheight... + -- SpawnTemplate.units[UnitID].alt = SpawnVec2: + self:T( 'After Translation SpawnTemplate.units[' .. UnitID .. '].x = ' .. UnitTemplate.x .. ', SpawnTemplate.units[' .. UnitID .. '].y = ' .. UnitTemplate.y ) + end + + SpawnTemplate.route.points[1].x = SpawnVec2.x + SpawnTemplate.route.points[1].y = SpawnVec2.y + SpawnTemplate.x = SpawnVec2.x + SpawnTemplate.y = SpawnVec2.y + + end + + return self +end + --- Private method that randomizes the @{Core.Zone}s where the Group will be spawned. -- @param #SPAWN self -- @param #number SpawnIndex @@ -3415,7 +3510,7 @@ end -- TODO Need to delete this... _DATABASE does this now ... ---- @param #SPAWN self +-- @param #SPAWN self -- @param Core.Event#EVENTDATA EventData function SPAWN:_OnBirth( EventData ) self:F( self.SpawnTemplatePrefix ) @@ -3435,7 +3530,7 @@ function SPAWN:_OnBirth( EventData ) end ---- @param #SPAWN self +-- @param #SPAWN self -- @param Core.Event#EVENTDATA EventData function SPAWN:_OnDeadOrCrash( EventData ) self:F( self.SpawnTemplatePrefix ) diff --git a/Moose Development/Moose/Core/Spot.lua b/Moose Development/Moose/Core/Spot.lua index d87606937..af00fee8f 100644 --- a/Moose Development/Moose/Core/Spot.lua +++ b/Moose Development/Moose/Core/Spot.lua @@ -33,7 +33,8 @@ do - --- @type SPOT + --- + -- @type SPOT -- @extends Core.Fsm#FSM @@ -228,7 +229,8 @@ do -- @param #number LaserCode Laser code. -- @param #number Duration Duration of lasing in seconds. function SPOT:onafterLaseOn( From, Event, To, Target, LaserCode, Duration ) - self:F( { "LaseOn", Target, LaserCode, Duration } ) + self:T({From, Event, To}) + self:T2( { "LaseOn", Target, LaserCode, Duration } ) local function StopLase( self ) self:LaseOff() @@ -256,6 +258,8 @@ do self:HandleEvent( EVENTS.Dead ) self:__Lasing( -1 ) + + return self end @@ -268,7 +272,7 @@ do -- @param #number LaserCode Laser code. -- @param #number Duration Duration of lasing in seconds. function SPOT:onafterLaseOnCoordinate(From, Event, To, Coordinate, LaserCode, Duration) - self:F( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) + self:T2( { "LaseOnCoordinate", Coordinate, LaserCode, Duration } ) local function StopLase( self ) self:LaseOff() @@ -290,12 +294,14 @@ do end self:__Lasing(-1) + return self end - - --- @param #SPOT self + + --- + -- @param #SPOT self -- @param Core.Event#EVENTDATA EventData function SPOT:OnEventDead(EventData) - self:F( { Dead = EventData.IniDCSUnitName, Target = self.Target } ) + self:T2( { Dead = EventData.IniDCSUnitName, Target = self.Target } ) if self.Target then if EventData.IniDCSUnitName == self.TargetName then self:F( {"Target dead ", self.TargetName } ) @@ -309,42 +315,51 @@ do self:LaseOff() end end + return self end - --- @param #SPOT self + --- + -- @param #SPOT self -- @param From -- @param Event -- @param To function SPOT:onafterLasing( From, Event, To ) - - if self.Target and self.Target:IsAlive() then - self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) - self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) - self:__Lasing( -0.2 ) - elseif self.TargetCoord then + self:T({From, Event, To}) - -- Wiggle the IR spot a bit. - local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 - local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 + if self.Lasing then + if self.Target and self.Target:IsAlive() then + + self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() ) + self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() ) + + self:__Lasing(0.2) + elseif self.TargetCoord then - self.SpotIR:setPoint(irvec3) - self.SpotLaser:setPoint(lsvec3) - - self:__Lasing(-0.25) - else - self:F( { "Target is not alive", self.Target:IsAlive() } ) + -- Wiggle the IR spot a bit. + local irvec3={x=self.TargetCoord.x+math.random(-100,100)/100, y=self.TargetCoord.y+math.random(-100,100)/100, z=self.TargetCoord.z} --#DCS.Vec3 + local lsvec3={x=self.TargetCoord.x, y=self.TargetCoord.y, z=self.TargetCoord.z} --#DCS.Vec3 + + self.SpotIR:setPoint(irvec3) + self.SpotLaser:setPoint(lsvec3) + + self:__Lasing(0.2) + else + self:F( { "Target is not alive", self.Target:IsAlive() } ) + end end - + return self end - - --- @param #SPOT self + + --- + -- @param #SPOT self -- @param From -- @param Event -- @param To -- @return #SPOT function SPOT:onafterLaseOff( From, Event, To ) - - self:F( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) + self:T({From, Event, To}) + + self:T2( {"Stopped lasing for ", self.Target and self.Target:GetName() or "coord", SpotIR = self.SportIR, SpotLaser = self.SpotLaser } ) self.Lasing = false @@ -383,4 +398,4 @@ do return self end -end \ No newline at end of file +end diff --git a/Moose Development/Moose/Functional/AICSAR.lua b/Moose Development/Moose/Functional/AICSAR.lua index c9ec13298..b71050778 100644 --- a/Moose Development/Moose/Functional/AICSAR.lua +++ b/Moose Development/Moose/Functional/AICSAR.lua @@ -22,7 +22,7 @@ -- === -- -- ### Author: **Applevangelist** --- Last Update February 2022 +-- Last Update July 2023 -- -- === -- @module Functional.AICSAR @@ -191,7 +191,7 @@ -- @field #AICSAR AICSAR = { ClassName = "AICSAR", - version = "0.1.14", + version = "0.1.15", lid = "", coalition = coalition.side.BLUE, template = "", @@ -397,6 +397,7 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) self:AddTransition("*", "PilotRescued", "*") -- Pilot Rescued self:AddTransition("*", "PilotKIA", "*") -- Pilot dead self:AddTransition("*", "HeloDown", "*") -- Helo dead + self:AddTransition("*", "HeloOnDuty", "*") -- Helo spawnd self:AddTransition("*", "Stop", "Stopped") -- Stop FSM. self:HandleEvent(EVENTS.LandingAfterEjection,self._EventHandler) @@ -473,6 +474,14 @@ function AICSAR:New(Alias,Coalition,Pilottemplate,Helotemplate,FARP,MASHZone) -- @param #string Event Event. -- @param #string To To state. + --- On after "HeloOnDuty" event. + -- @function [parent=#AICSAR] OnAfterHeloOnDuty + -- @param #AICSAR self + -- @param #string From From state. + -- @param #string Event Event. + -- @param #string To To state. + -- @param Wrapper.Group#GROUP Helo Helo group object + --- On after "HeloDown" event. -- @function [parent=#AICSAR] OnAfterHeloDown -- @param #AICSAR self @@ -853,6 +862,11 @@ function AICSAR:_GetFlight() local newhelo = SPAWN:NewWithAlias(self.helotemplate,self.helotemplate..math.random(1,10000)) :InitDelayOff() :InitUnControlled(true) + :OnSpawnGroup( + function(Group) + self:__HeloOnDuty(1,Group) + end + ) :Spawn() local nhelo=FLIGHTGROUP:New(newhelo) diff --git a/Moose Development/Moose/Functional/AmmoTruck.lua b/Moose Development/Moose/Functional/AmmoTruck.lua index e33ae8a52..83b177bef 100644 --- a/Moose Development/Moose/Functional/AmmoTruck.lua +++ b/Moose Development/Moose/Functional/AmmoTruck.lua @@ -17,7 +17,7 @@ -- @module Functional.AmmoTruck -- @image Artillery.JPG -- --- Date: Nov 2022 +-- Last update: July 2023 ------------------------------------------------------------------------- --- **AMMOTRUCK** class, extends Core.FSM#FSM @@ -40,6 +40,7 @@ -- @field #number unloadtime Unload time in seconds -- @field #number waitingtime Max waiting time in seconds -- @field #boolean routeonroad Route truck on road if true (default) +-- @field #number reloads Number of reloads a single truck can do before he must return home -- @extends Core.FSM#FSM --- *Amateurs talk about tactics, but professionals study logistics.* - General Robert H Barrow, USMC @@ -73,9 +74,10 @@ -- ammotruck.remunidist = 20000 -- 20km - send trucks max this far from home -- ammotruck.unloadtime = 600 -- 10 minutes - min time to unload ammunition -- ammotruck.waitingtime = 1800 -- 30 mintes - wait max this long until remunition is done --- ammotruck.monitor = -60 - 1 minute - AMMOTRUCK checks on things every 1 minute --- ammotruck.routeonroad = true - Trucks will **try** to drive on roads --- ammotruck.usearmygroup = false - if true, will make use of ARMYGROUP in the background (if used in DEV branch) +-- ammotruck.monitor = -60 -- 1 minute - AMMOTRUCK checks run every one minute +-- ammotruck.routeonroad = true -- Trucks will **try** to drive on roads +-- ammotruck.usearmygroup = false -- If true, will make use of ARMYGROUP in the background (if used in DEV branch) +-- ammotruck.reloads = 5 -- Maxn re-arms a truck can do before he needs to go home and restock. Set to -1 for unlimited -- -- ## 3 FSM Events to shape mission -- @@ -113,7 +115,7 @@ AMMOTRUCK = { ClassName = "AMMOTRUCK", lid = "", - version = "0.0.10", + version = "0.0.12", alias = "", debug = false, trucklist = {}, @@ -128,7 +130,8 @@ AMMOTRUCK = { monitor = -60, unloadtime = 600, waitingtime = 1800, - routeonroad = true + routeonroad = true, + reloads = 5, } --- @@ -156,6 +159,7 @@ AMMOTRUCK.State = { --@field #string targetname --@field Wrapper.Group#GROUP targetgroup --@field Core.Point#COORDINATE targetcoordinate +--@field #number reloads --- -- @param #AMMOTRUCK self @@ -369,6 +373,7 @@ function AMMOTRUCK:CheckReturningTrucks(dataset) truck.statusquo = AMMOTRUCK.State.IDLE truck.timestamp = timer.getAbsTime() truck.coordinate = coord + truck.reloads = self.reloads or 5 self:__TruckHome(1,truck) end end @@ -540,6 +545,7 @@ function AMMOTRUCK:CheckTrucksAlive() newtruck.statusquo = AMMOTRUCK.State.IDLE newtruck.timestamp = timer.getAbsTime() newtruck.coordinate = truck:GetCoordinate() + newtruck.reloads = self.reloads or 5 self.trucklist[name] = newtruck end end @@ -626,8 +632,10 @@ function AMMOTRUCK:onafterMonitor(From, Event, To) unloadingtrucks[#unloadingtrucks+1] = data elseif data.statusquo == AMMOTRUCK.State.RETURNING then returningtrucks[#returningtrucks+1] = data + if data.reloads > 0 or data.reloads == -1 then idletrucks[#idletrucks+1] = data found = true + end end else self.truckset[data.name] = nil @@ -637,7 +645,7 @@ function AMMOTRUCK:onafterMonitor(From, Event, To) local n=0 if found and remunition then -- match - local match = false + --local match = false for _,_truckdata in pairs(idletrucks) do local truckdata = _truckdata -- #AMMOTRUCK.data local truckcoord = truckdata.group:GetCoordinate() -- Core.Point#COORDINATE @@ -750,6 +758,12 @@ end end local scheduler = SCHEDULER:New(nil,destroyammo,{ammo},self.waitingtime) + + -- one reload less + if truck.reloads ~= -1 then + truck.reloads = truck.reloads - 1 + end + return self end --- diff --git a/Moose Development/Moose/Functional/Detection.lua b/Moose Development/Moose/Functional/Detection.lua index e7f76ef0f..3558354a1 100644 --- a/Moose Development/Moose/Functional/Detection.lua +++ b/Moose Development/Moose/Functional/Detection.lua @@ -2354,6 +2354,7 @@ do -- DETECTION_TYPES if not DetectedItem then DetectedItem = self:AddDetectedItem( "TYPE", DetectedTypeName ) DetectedItem.TypeName = DetectedTypeName + DetectedItem.Name = DetectedUnitName -- fix by @Nocke end DetectedItem.Set:AddUnit( DetectedUnit ) diff --git a/Moose Development/Moose/Functional/Mantis.lua b/Moose Development/Moose/Functional/Mantis.lua index 3f6f7b008..3cfbaa4c9 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: Oct 2022 +-- Last Update: July 2023 ------------------------------------------------------------------------- --- **MANTIS** class, extends Core.Base#BASE @@ -104,9 +104,13 @@ -- * Silkworm (though strictly speaking this is a surface to ship missile) -- * SA-2, SA-3, SA-5, SA-6, SA-7, SA-8, SA-9, SA-10, SA-11, SA-13, SA-15, SA-19 -- * From HDS (see note on HDS below): SA-2, SA-3, SA-10B, SA-10C, SA-12, SA-17, SA-20A, SA-20B, SA-23, HQ-2 +-- -- * From SMA: RBS98M, RBS70, RBS90, RBS90M, RBS103A, RBS103B, RBS103AM, RBS103BM, Lvkv9040M -- **NOTE** If you are using the Swedish Military Assets (SMA), please note that the **group name** for RBS-SAM types also needs to contain the keyword "SMA" -- +-- * From CH: 2S38, PantsirS1, PantsirS2, PGL-625, HQ-17A, M903PAC2, M903PAC3, TorM2, TorM2K, TorM2M, NASAMS3-AMRAAMER, NASAMS3-AIM9X2, C-RAM, PGZ-09, S350-9M100, S350-9M96D +-- **NOTE** If you are using the Military Assets by Currenthill (CH), please note that the **group name** for CH-SAM types also needs to contain the keyword "CHM" +-- -- Following the example started above, an SA-6 site group name should start with "Red SAM SA-6" then, or a blue Patriot installation with e.g. "Blue SAM Patriot". -- **NOTE** If you are using the High-Digit-Sam Mod, please note that the **group name** for the following SAM types also needs to contain the keyword "HDS": -- @@ -369,6 +373,7 @@ MANTIS.SamData = { ["SA-20A"] = { Range=150, Blindspot=5, Height=27, Type="Long" , Radar="S-300PMU1"}, ["SA-20B"] = { Range=200, Blindspot=4, Height=27, Type="Long" , Radar="S-300PMU2"}, ["HQ-2"] = { Range=50, Blindspot=6, Height=35, Type="Medium", Radar="HQ_2_Guideline_LN" }, + ["SHORAD"] = { Range=3, Blindspot=0, Height=3, Type="Short", Radar="Igla" } } --- SAM data HDS @@ -415,6 +420,35 @@ MANTIS.SamDataSMA = { ["Lvkv9040M SMA"] = { Range=4, Blindspot=0, Height=2.5, Type="Short", Radar="LvKv9040" }, } +--- SAM data CH +-- @type MANTIS.SamDataCH +-- @field #number Range Max firing range in km +-- @field #number Blindspot no-firing range (green circle) +-- @field #number Height Max firing height in km +-- @field #string Type #MANTIS.SamType of SAM, i.e. SHORT, MEDIUM or LONG (range) +-- @field #string Radar Radar typename on unit level (used as key) +MANTIS.SamDataCH = { + -- units from CH (Military Assets by Currenthill) + -- https://www.currenthill.com/ + -- group name MUST contain CHM to ID launcher type correctly! + ["2S38 CH"] = { Range=8, Blindspot=0.5, Height=6, Type="Short", Radar="2S38" }, + ["PantsirS1 CH"] = { Range=20, Blindspot=1.2, Height=15, Type="Short", Radar="PantsirS1" }, + ["PantsirS2 CH"] = { Range=30, Blindspot=1.2, Height=18, Type="Medium", Radar="PantsirS2" }, + ["PGL-625 CH"] = { Range=10, Blindspot=0.5, Height=5, Type="Short", Radar="PGL_625" }, + ["HQ-17A CH"] = { Range=20, Blindspot=1.5, Height=10, Type="Short", Radar="HQ17A" }, + ["M903PAC2 CH"] = { Range=160, Blindspot=3, Height=24.5, Type="Long", Radar="MIM104_M903_PAC2" }, + ["M903PAC3 CH"] = { Range=120, Blindspot=1, Height=40, Type="Long", Radar="MIM104_M903_PAC3" }, + ["TorM2 CH"] = { Range=12, Blindspot=1, Height=10, Type="Short", Radar="TorM2" }, + ["TorM2K CH"] = { Range=12, Blindspot=1, Height=10, Type="Short", Radar="TorM2K" }, + ["TorM2M CH"] = { Range=16, Blindspot=1, Height=10, Type="Short", Radar="TorM2M" }, + ["NASAMS3-AMRAAMER CH"] = { Range=50, Blindspot=2, Height=35.7, Type="Medium", Radar="CH_NASAMS3_LN_AMRAAM_ER" }, + ["NASAMS3-AIM9X2 CH"] = { Range=20, Blindspot=0.2, Height=18, Type="Short", Radar="CH_NASAMS3_LN_AIM9X2" }, + ["C-RAM CH"] = { Range=2, Blindspot=0, Height=2, Type="Short", Radar="CH_Centurion_C_RAM" }, + ["PGZ-09 CH"] = { Range=4, Blindspot=0, Height=3, Type="Short", Radar="CH_PGZ09" }, + ["S350-9M100 CH"] = { Range=15, Blindspot=1.5, Height=8, Type="Short", Radar="CH_S350_50P6_9M100" }, + ["S350-9M96D CH"] = { Range=150, Blindspot=2.5, Height=30, Type="Long", Radar="CH_S350_50P6_9M96D" }, +} + ----------------------------------------------------------------------- -- MANTIS System ----------------------------------------------------------------------- @@ -578,7 +612,7 @@ do -- TODO Version -- @field #string version - self.version="0.8.9" + self.version="0.8.11" self:I(string.format("***** Starting MANTIS Version %s *****", self.version)) --- FSM Functions --- @@ -1299,11 +1333,12 @@ do -- @param #string grpname Name of the group -- @param #boolean mod HDS mod flag -- @param #boolean sma SMA mod flag + -- @param #boolean chm CH mod flag -- @return #number range Max firing range -- @return #number height Max firing height -- @return #string type Long, medium or short range -- @return #number blind "blind" spot - function MANTIS:_GetSAMDataFromUnits(grpname,mod,sma) + function MANTIS:_GetSAMDataFromUnits(grpname,mod,sma,chm) self:T(self.lid.."_GetSAMRangeFromUnits") local found = false local range = self.checkradius @@ -1318,8 +1353,10 @@ do SAMData = self.SamDataHDS elseif sma then SAMData = self.SamDataSMA + elseif chm then + SAMData = self.SamDataCH end - --self:I("Looking to auto-match for "..grpname) + --self:T("Looking to auto-match for "..grpname) for _,_unit in pairs(units) do local unit = _unit -- Wrapper.Unit#UNIT local type = string.lower(unit:GetTypeName()) @@ -1364,10 +1401,13 @@ do local found = false local HDSmod = false local SMAMod = false + local CHMod = false if string.find(grpname,"HDS",1,true) then HDSmod = true elseif string.find(grpname,"SMA",1,true) then SMAMod = true + elseif string.find(grpname,"CHM",1,true) then + CHMod = true end if self.automode then for idx,entry in pairs(self.SamData) do @@ -1386,8 +1426,8 @@ do end end -- secondary filter if not found - if (not found and self.automode) or HDSmod or SMAMod then - range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod) + if (not found and self.automode) or HDSmod or SMAMod or CHMod then + range, height, type = self:_GetSAMDataFromUnits(grpname,HDSmod,SMAMod,CHMod) elseif not found then self:E(self.lid .. string.format("*****Could not match radar data for %s! Will default to midrange values!",grpname)) end diff --git a/Moose Development/Moose/Functional/Scoring.lua b/Moose Development/Moose/Functional/Scoring.lua index 950ccd3c0..3d0c56b9f 100644 --- a/Moose Development/Moose/Functional/Scoring.lua +++ b/Moose Development/Moose/Functional/Scoring.lua @@ -1767,9 +1767,9 @@ function SCORING:SecondsToClock( sSeconds ) -- return nil; return "00:00:00"; else - nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) ); - nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) ); - nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); + local nHours = string.format( "%02.f", math.floor( nSeconds / 3600 ) ); + local nMins = string.format( "%02.f", math.floor( nSeconds / 60 - (nHours * 60) ) ); + local nSecs = string.format( "%02.f", math.floor( nSeconds - nHours * 3600 - nMins * 60 ) ); return nHours .. ":" .. nMins .. ":" .. nSecs end end diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 3d9f8f981..d5cad1818 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -12,6 +12,7 @@ __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) __Moose.Include( 'Scripts/Moose/Core/Astar.lua' ) __Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) __Moose.Include( 'Scripts/Moose/Core/Condition.lua' ) +__Moose.Include( 'Scripts/Moose/Core/ClientMenu.lua') __Moose.Include( 'Scripts/Moose/Core/Database.lua' ) __Moose.Include( 'Scripts/Moose/Core/Event.lua' ) __Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 3780ba573..719ad8da7 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: cast-local-type --- **Ops** - Automatic Terminal Information Service (ATIS). -- -- === @@ -608,15 +609,16 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.9.14" +ATIS.version = "0.9.15" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: Add new Normandy airfields. --- TODO: Zulu time --> Zulu in output. -- TODO: Correct fog for elevation. +-- DONE: Zulu time --> Zulu in output. +-- DONE: Fix for AB not having a runway - Helopost like Naqoura +-- DONE: Add new Normandy airfields. -- DONE: Use new AIRBASE system to set start/landing runway -- DONE: SetILS doesn't work -- DONE: Visibility reported twice over SRS @@ -2139,16 +2141,20 @@ function ATIS:onafterBroadcast( From, Event, To ) end end alltext = alltext .. ";\n" .. subtitle + + local _RUNACT if not self.ATISforFARPs then -- Active runway. - local subtitle=string.format("Active runway %s", runwayLanding) - if rwyLandingLeft==true then - subtitle=subtitle.." Left" - elseif rwyLandingLeft==false then - subtitle=subtitle.." Right" + if runwayLanding then + local subtitle=string.format("Active runway %s", runwayLanding) + if rwyLandingLeft==true then + subtitle=subtitle.." Left" + elseif rwyLandingLeft==false then + subtitle=subtitle.." Right" + end end - local _RUNACT = subtitle + _RUNACT = subtitle if not self.useSRS then self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) self.radioqueue:Number2Transmission(runwayLanding) @@ -2509,8 +2515,11 @@ function ATIS:GetActiveRunway(Takeoff) else runway=self.airbase:GetActiveRunwayLanding() end - - return runway.name, runway.isLeft + if runway then -- some ABs have NO runways, e.g. Syria Naqoura + return runway.name, runway.isLeft + else + return nil, nil + end end --- Get runway from user supplied magnetic heading. diff --git a/Moose Development/Moose/Ops/Airboss.lua b/Moose Development/Moose/Ops/Airboss.lua index 3cc61f949..005f5b614 100644 --- a/Moose Development/Moose/Ops/Airboss.lua +++ b/Moose Development/Moose/Ops/Airboss.lua @@ -10266,7 +10266,7 @@ function AIRBOSS:_GetSternCoord() elseif case==2 or case==1 then -- V/Stol: Translate 8 meters port. self.sterncoord:Translate(self.carrierparam.sterndist, hdg, true, true):Translate(8, FB-90, true, true) - end + end elseif self.carriertype==AIRBOSS.CarrierType.STENNIS then -- Stennis: translate 7 meters starboard wrt Final bearing. self.sterncoord:Translate( self.carrierparam.sterndist, hdg, true, true ):Translate( 7, FB + 90, true, true ) @@ -11585,7 +11585,13 @@ function AIRBOSS:GetHeadingIntoWind( magnetic, coord ) end -- Get direction the wind is blowing from. This is where we want to go. - local windfrom, vwind = self:GetWind( nil, nil, coord ) + adjustDegreesForWindSpeed(vwind) + local windfrom, vwind = self:GetWind( nil, nil, coord ) + + --self:I("windfrom="..windfrom.." vwind="..vwind) + + vwind = vwind + adjustDegreesForWindSpeed(vwind) + + --self:I("windfrom="..windfrom.." (c)vwind="..vwind) -- Actually, we want the runway in the wind. local intowind = windfrom - self.carrierparam.rwyangle @@ -17348,7 +17354,7 @@ function AIRBOSS:_DisplayCarrierInfo( _unitname ) state = "Deck closed" end if self.turning then - state = state .. " (turning currently)" + state = state .. " (currently turning)" end -- Message text. diff --git a/Moose Development/Moose/Ops/Auftrag.lua b/Moose Development/Moose/Ops/Auftrag.lua index 00fb6ddbd..7d4deac86 100644 --- a/Moose Development/Moose/Ops/Auftrag.lua +++ b/Moose Development/Moose/Ops/Auftrag.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: undefined-global --- **Ops** - Auftrag (mission) for Ops. -- -- ## Main Features: @@ -259,6 +260,10 @@ -- -- Not implemented yet. -- +-- ## Ground Escort +-- +-- An escort mission can be created with the @{#AUFTRAG.NewGROUNDESCORT}() function. +-- -- ## Intercept -- -- An intercept mission can be created with the @{#AUFTRAG.NewINTERCEPT}() function. @@ -406,6 +411,7 @@ _AUFTRAGSNR=0 -- @field #string FAC Forward AirController mission. -- @field #string FACA Forward AirController airborne mission. -- @field #string FERRY Ferry mission. +-- @field #string GROUNDESCORT Ground escort mission. -- @field #string INTERCEPT Intercept mission. -- @field #string ORBIT Orbit mission. -- @field #string GCICAP Similar to CAP but no auto engage targets. @@ -450,6 +456,7 @@ AUFTRAG.Type={ FAC="FAC", FACA="FAC-A", FERRY="Ferry Flight", + GROUNDESCORT="Ground Escort", INTERCEPT="Intercept", ORBIT="Orbit", GCICAP="Ground Controlled CAP", @@ -477,7 +484,7 @@ AUFTRAG.Type={ RELOCATECOHORT="Relocate Cohort", AIRDEFENSE="Air Defence", EWR="Early Warning Radar", - RECOVERYTANKER="Recovery Tanker", + --RECOVERYTANKER="Recovery Tanker", REARMING="Rearming", CAPTUREZONE="Capture Zone", NOTHING="Nothing", @@ -1740,6 +1747,43 @@ function AUFTRAG:NewBOMBCARPET(Target, Altitude, CarpetLength) return mission end +--- **[AIR/HELO]** Create a GROUNDESCORT (or FOLLOW) mission. Helo will escort a **ground** group and automatically engage certain target types. +-- @param #AUFTRAG self +-- @param Wrapper.Group#GROUP EscortGroup The ground group to escort. +-- @param #number OrbitDistance Orbit to/from the lead unit this many NM. Defaults to 1.5 NM. +-- @param #table TargetTypes Types of targets to engage automatically. Default is {"Ground vehicles"}, i.e. all enemy ground units. Use an empty set {} for a simple "FOLLOW" mission. +-- @return #AUFTRAG self +function AUFTRAG:NewGROUNDESCORT(EscortGroup, OrbitDistance, TargetTypes) + + local mission=AUFTRAG:New(AUFTRAG.Type.GROUNDESCORT) + + -- If only a string is passed we set a variable and check later if the group exists. + if type(EscortGroup)=="string" then + mission.escortGroupName=EscortGroup + mission:_TargetFromObject() + else + mission:_TargetFromObject(EscortGroup) + end + + -- DCS task parameters: + mission.orbitDistance=OrbitDistance and UTILS.NMToMeters(OrbitDistance) or UTILS.NMToMeters(1.5) + --mission.engageMaxDistance=EngageMaxDistance and UTILS.NMToMeters(EngageMaxDistance) or UTILS.NMToMeters(5) + mission.engageTargetTypes=TargetTypes or {"Ground vehicles"} + + -- Mission options: + mission.missionTask=ENUMS.MissionTask.GROUNDESCORT + mission.missionFraction=0.1 + mission.missionAltitude=100 + mission.optionROE=ENUMS.ROE.OpenFire -- TODO: what's the best ROE here? Make dependent on ESCORT or FOLLOW! + mission.optionROT=ENUMS.ROT.EvadeFire + + mission.categories={AUFTRAG.Category.HELICOPTER} + + mission.DCStask=mission:GetDCSMissionTask() + + return mission +end + --- **[AIR]** Create an ESCORT (or FOLLOW) mission. Flight will escort another group and automatically engage certain target types. -- @param #AUFTRAG self @@ -5843,10 +5887,20 @@ function AUFTRAG:GetDCSMissionTask() -- ESCORT Mission -- -------------------- - local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, LastWaypointIndex, self.engageMaxDistance, self.engageTargetTypes) + local DCStask=CONTROLLABLE.TaskEscort(nil, self.engageTarget:GetObject(), self.escortVec3, nil, self.engageMaxDistance, self.engageTargetTypes) table.insert(DCStasks, DCStask) + + elseif self.type==AUFTRAG.Type.GROUNDESCORT then + -------------------- + -- GROUNDESCORT Mission -- + -------------------- + + local DCSTask=CONTROLLABLE.TaskGroundEscort(nil,self.engageTarget:GetObject(),nil,self.orbitDistance,self.engageTargetTypes) + + table.insert(DCStasks, DCSTask) + elseif self.type==AUFTRAG.Type.FACA then ------------------ @@ -6528,6 +6582,8 @@ function AUFTRAG:GetMissionTaskforMissionType(MissionType) mtask=ENUMS.MissionTask.AFAC elseif MissionType==AUFTRAG.Type.FERRY then mtask=ENUMS.MissionTask.NOTHING + elseif MissionType==AUFTRAG.Type.GROUNDESCORT then + mtask=ENUMS.MissionTask.GROUNDESCORT elseif MissionType==AUFTRAG.Type.INTERCEPT then mtask=ENUMS.MissionTask.INTERCEPT elseif MissionType==AUFTRAG.Type.RECON then diff --git a/Moose Development/Moose/Ops/Awacs.lua b/Moose Development/Moose/Ops/Awacs.lua index 7da32be2f..7eaf70fa1 100644 --- a/Moose Development/Moose/Ops/Awacs.lua +++ b/Moose Development/Moose/Ops/Awacs.lua @@ -499,7 +499,7 @@ do -- @field #AWACS AWACS = { ClassName = "AWACS", -- #string - version = "0.2.54", -- #string + version = "0.2.55", -- #string lid = "", -- #string coalition = coalition.side.BLUE, -- #number coalitiontxt = "blue", -- #string @@ -783,8 +783,8 @@ AWACS.Messages = { -- @field #string AwacsStateMission -- @field #string AwacsStateFG -- @field #boolean AwacsShiftChange --- @field #string EscortsStateMission --- @field #string EscortsStateFG +-- @field #table EscortsStateMission +-- @field #table EscortsStateFG -- @field #boolean EscortsShiftChange -- @field #number AICAPMax -- @field #number AICAPCurrent @@ -1162,8 +1162,8 @@ function AWACS:New(Name,AirWing,Coalition,AirbaseName,AwacsOrbit,OpsZone,Station MonitoringData.AwacsStateFG = "unknown" MonitoringData.AwacsStateMission = "unknown" MonitoringData.EscortsShiftChange = false - MonitoringData.EscortsStateFG= "unknown" - MonitoringData.EscortsStateMission = "unknown" + MonitoringData.EscortsStateFG = {} + MonitoringData.EscortsStateMission = {} self.MonitoringOn = false -- #boolean self.MonitoringData = MonitoringData @@ -2027,9 +2027,9 @@ function AWACS:_StartEscorts(Shiftchange) self.CatchAllMissions[#self.CatchAllMissions+1] = escort if Shiftchange then - self.EscortMissionReplacement[i] = mission + self.EscortMissionReplacement[i] = escort else - self.EscortMission[i] = mission + self.EscortMission[i] = escort end end @@ -3597,10 +3597,13 @@ function AWACS:_SetClientMenus() local tasking = MENU_GROUP:New(cgrp,"Tasking",basemenu) local showtask = MENU_GROUP_COMMAND:New(cgrp,"Showtask",tasking,self._Showtask,self,cgrp) + local commit + local unable + local abort if self.PlayerCapAssignment then - local commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) - local unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) - local abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) + commit = MENU_GROUP_COMMAND:New(cgrp,"Commit",tasking,self._Commit,self,cgrp) + unable = MENU_GROUP_COMMAND:New(cgrp,"Unable",tasking,self._Unable,self,cgrp) + abort = MENU_GROUP_COMMAND:New(cgrp,"Abort",tasking,self._TaskAbort,self,cgrp) --local judy = MENU_GROUP_COMMAND:New(cgrp,"Judy",tasking,self._Judy,self,cgrp) end @@ -4933,8 +4936,8 @@ function AWACS:_AnnounceContact(Contact,IsNew,Group,IsBogeyDope,Tag,IsPopup,Repo end end - string.gsub(BRAText,"BRAA","brah") - string.gsub(BRAText,"BRA","brah") + BRAText = string.gsub(BRAText,"BRAA","brah") + BRAText = string.gsub(BRAText,"BRA","brah") local prio = IsNew or IsBogeyDope self:_NewRadioEntry(BRAText,TextScreen,GID,isGroup,true,IsNew,false,prio) @@ -5547,7 +5550,7 @@ end -- @param #string Event -- @param #string To -- @return #AWACS self -function AWACS:onbeforeStart(From,Event,to) +function AWACS:onbeforeStart(From,Event,To) self:T({From, Event, To}) if self.IncludeHelicopters then self.clientset:FilterCategories("helicopter") diff --git a/Moose Development/Moose/Ops/CTLD.lua b/Moose Development/Moose/Ops/CTLD.lua index 298f7b1dd..7e79bdbc8 100644 --- a/Moose Development/Moose/Ops/CTLD.lua +++ b/Moose Development/Moose/Ops/CTLD.lua @@ -204,7 +204,7 @@ CTLD_CARGO = { -- @param #CTLD_CARGO self -- @param #boolean loaded function CTLD_CARGO:Isloaded() - if self.HasBeenMoved and not self.WasDropped() then + if self.HasBeenMoved and not self:WasDropped() then return true else return false @@ -1221,7 +1221,7 @@ CTLD.UnitTypes = { --- CTLD class version. -- @field #string version -CTLD.version="1.0.39" +CTLD.version="1.0.40" --- Instantiate a new CTLD. -- @param #CTLD self @@ -1390,6 +1390,7 @@ function CTLD:New(Coalition, Prefixes, Alias) -- sub categories self.usesubcats = false self.subcats = {} + self.subcatsTroop = {} -- disallow building in loadzones self.nobuildinloadzones = true @@ -2279,6 +2280,7 @@ function CTLD:_GetCrates(Group, Unit, Cargo, number, drop) if not drop then inzone = self:IsUnitInZone(Unit,CTLD.CargoZoneType.LOAD) if not inzone then +---@diagnostic disable-next-line: cast-local-type inzone, ship, zone, distance, width = self:IsUnitInZone(Unit,CTLD.CargoZoneType.SHIP) end else @@ -3468,6 +3470,12 @@ function CTLD:_RefreshF10Menus() self.subcats[entry.Subcategory] = entry.Subcategory end end + for _id,_cargo in pairs(self.Cargo_Troops) do + local entry = _cargo -- #CTLD_CARGO + if not self.subcatsTroop[entry.Subcategory] then + self.subcatsTroop[entry.Subcategory] = entry.Subcategory + end + end end -- build unit menus @@ -3504,15 +3512,28 @@ function CTLD:_RefreshF10Menus() local beaconself = MENU_GROUP_COMMAND:New(_group,"Drop beacon now",smoketopmenu, self.DropBeaconNow, self, _unit):Refresh() -- sub menus -- sub menu troops management - if cantroops then + if cantroops then local troopsmenu = MENU_GROUP:New(_group,"Load troops",toptroops) - for _,_entry in pairs(self.Cargo_Troops) do - local entry = _entry -- #CTLD_CARGO - menucount = menucount + 1 - menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + if self.usesubcats then + local subcatmenus = {} + for _name,_entry in pairs(self.subcatsTroop) do + subcatmenus[_name] = MENU_GROUP:New(_group,_name,troopsmenu) + end + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + local subcat = entry.Subcategory + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,subcatmenus[subcat],self._LoadTroops, self, _group, _unit, entry) + end + else + for _,_entry in pairs(self.Cargo_Troops) do + local entry = _entry -- #CTLD_CARGO + menucount = menucount + 1 + menus[menucount] = MENU_GROUP_COMMAND:New(_group,entry.Name,troopsmenu,self._LoadTroops, self, _group, _unit, entry) + end end local unloadmenu1 = MENU_GROUP_COMMAND:New(_group,"Drop troops",toptroops, self._UnloadTroops, self, _group, _unit):Refresh() - local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() + local extractMenu1 = MENU_GROUP_COMMAND:New(_group, "Extract troops", toptroops, self._ExtractTroops, self, _group, _unit):Refresh() end -- sub menu crates management if cancrates then @@ -3603,7 +3624,8 @@ end -- @param #number NoTroops Size of the group in number of Units across combined templates (for loading). -- @param #number PerTroopMass Mass in kg of each soldier -- @param #number Stock Number of groups in stock. Nil for unlimited. -function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) +-- @param #string SubCategory Name of sub-category (optional). +function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock,SubCategory) self:T(self.lid .. " AddTroopsCargo") self:T({Name,Templates,Type,NoTroops,PerTroopMass,Stock}) if not self:_CheckTemplates(Templates) then @@ -3612,7 +3634,7 @@ function CTLD:AddTroopsCargo(Name,Templates,Type,NoTroops,PerTroopMass,Stock) end self.CargoCounter = self.CargoCounter + 1 -- Troops are directly loadable - local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock) + local cargo = CTLD_CARGO:New(self.CargoCounter,Name,Templates,Type,false,true,NoTroops,nil,nil,PerTroopMass,Stock, SubCategory) table.insert(self.Cargo_Troops,cargo) return self end diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua index a7c127b1f..196e422d0 100644 --- a/Moose Development/Moose/Ops/PlayerTask.lua +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -522,7 +522,7 @@ function PLAYERTASK:MarkTargetOnF10Map(Text,Coalition,ReadOnly) -- Marker exists, delete one first self.TargetMarker:Remove() end - local text = Text or "Target of "..self.lid + local text = Text or ("Target of "..self.lid) self.TargetMarker = MARKER:New(coordinate,text) if ReadOnly then self.TargetMarker:ReadOnly() @@ -954,6 +954,7 @@ do -- @field Utilities.FiFo#FIFO TasksPerPlayer -- @field Utilities.FiFo#FIFO PrecisionTasks -- @field Core.Set#SET_CLIENT ClientSet +-- @field Core.Set#SET_CLIENT ActiveClientSet -- @field #string ClientFilter -- @field #string Name -- @field #string Type @@ -988,6 +989,7 @@ do -- @field #table PlayerJoinMenu -- @field #table PlayerInfoMenu -- @field #boolean noflaresmokemenu +-- @field #boolean illumenu -- @field #boolean TransmitOnlyWithPlayers -- @field #boolean buddylasing -- @field Ops.PlayerRecce#PLAYERRECCE PlayerRecce @@ -999,6 +1001,13 @@ do -- @field #table PlayerMenuTag -- @field #boolean UseTypeNames -- @field Functional.Scoring#SCORING Scoring +-- @field Core.ClientMenu#CLIENTMENUMANAGER JoinTaskMenuTemplate +-- @field Core.ClientMenu#CLIENTMENU JoinMenu +-- @field Core.ClientMenu#CLIENTMENU JoinTopMenu +-- @field Core.ClientMenu#CLIENTMENU JoinInfoMenu +-- @field Core.ClientMenu#CLIENTMENUMANAGER ActiveTaskMenuTemplate +-- @field Core.ClientMenu#CLIENTMENU ActiveTopMenu +-- @field Core.ClientMenu#CLIENTMENU ActiveInfoMenu -- @extends Core.Fsm#FSM --- @@ -1155,6 +1164,7 @@ do -- MENUMARK = "Mark on map", -- MENUSMOKE = "Smoke", -- MENUFLARE = "Flare", +-- MENUILLU = "Illuminate", -- MENUABORT = "Abort", -- MENUJOIN = "Join Task", -- MENUTASKINFO = Task Info", @@ -1315,6 +1325,7 @@ PLAYERTASKCONTROLLER = { PlayerInfoMenu = {}, PlayerMenuTag = {}, noflaresmokemenu = false, + illumenu = false, TransmitOnlyWithPlayers = true, buddylasing = false, PlayerRecce = nil, @@ -1408,6 +1419,7 @@ PLAYERTASKCONTROLLER.Messages = { MENUMARK = "Mark on map", MENUSMOKE = "Smoke", MENUFLARE = "Flare", + MENUILLU = "Illuminate", MENUABORT = "Abort", MENUJOIN = "Join Task", MENUTASKINFO = "Task Info", @@ -1487,6 +1499,7 @@ PLAYERTASKCONTROLLER.Messages = { MENUMARK = "Kartenmarkierung", MENUSMOKE = "Rauchgranate", MENUFLARE = "Leuchtgranate", + MENUILLU = "Feldbeleuchtung", MENUABORT = "Abbrechen", MENUJOIN = "Auftrag annehmen", MENUTASKINFO = "Auftrag Briefing", @@ -1592,25 +1605,28 @@ function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) self.CallsignTranslations = nil self.noflaresmokemenu = false + self.illumenu = false self.ShowMagnetic = true self.UseTypeNames = false - local IsClientSet = false + self.IsClientSet = false if ClientFilter and type(ClientFilter) == "table" and ClientFilter.ClassName and ClientFilter.ClassName == "SET_CLIENT" then -- we have a predefined SET_CLIENT self.ClientSet = ClientFilter - IsClientSet = true + self.IsClientSet = true end - if ClientFilter and not IsClientSet then + if ClientFilter and not self.IsClientSet then self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() - elseif not IsClientSet then + elseif not self.IsClientSet then self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() end + self.ActiveClientSet = SET_CLIENT:New() + self.lid=string.format("PlayerTaskController %s %s | ", self.Name, tostring(self.Type)) self:_InitLocalization() @@ -1849,6 +1865,24 @@ function PLAYERTASKCONTROLLER:SetEnableSmokeFlareTask() return self end +--- [User] Show menu entries to illuminate targets. Needs smoke/flare enabled. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetEnableIlluminateTask() + self:T(self.lid.."SetEnableSmokeFlareTask") + self.illumenu = true + return self +end + +--- [User] Do not show menu entries to illuminate targets. +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SetDisableIlluminateTask() + self:T(self.lid.."SetDisableIlluminateTask") + self.illumenu = false + return self +end + --- [User] Show info text on screen with a coordinate info in any case (OFF by default) -- @param #PLAYERTASKCONTROLLER self -- @param #boolean OnOff Switch on = true or off = false @@ -2141,6 +2175,7 @@ end -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:_EventHandler(EventData) self:T(self.lid.."_EventHandler: "..EventData.id) + self:T(self.lid.."_EventHandler: "..EventData.IniPlayerName) if EventData.id == EVENTS.PlayerLeaveUnit or EventData.id == EVENTS.Ejection or EventData.id == EVENTS.Crash or EventData.id == EVENTS.PilotDead then if EventData.IniPlayerName then self:T(self.lid.."Event for player: "..EventData.IniPlayerName) @@ -2164,41 +2199,45 @@ function PLAYERTASKCONTROLLER:_EventHandler(EventData) self:T(self.lid..text) end elseif EventData.id == EVENTS.PlayerEnterAircraft and EventData.IniCoalition == self.Coalition then - if EventData.IniPlayerName and EventData.IniGroup and self.UseSRS then - if self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then + if EventData.IniPlayerName and EventData.IniGroup then + if self.IsClientSet and self.ClientSet:IsNotInSet(CLIENT:FindByName(EventData.IniUnitName)) then + self:T(self.lid.."Client not in SET: "..EventData.IniPlayerName) return self end self:T(self.lid.."Event for player: "..EventData.IniPlayerName) - local frequency = self.Frequency - local freqtext = "" - if type(frequency) == "table" then - freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale) - freqtext = freqtext..table.concat(frequency,", ") - else - local freqt = self.gettext:GetEntry("FREQUENCY",self.locale) - freqtext = string.format(freqt,frequency) - end - local modulation = self.Modulation - if type(modulation) == "table" then modulation = modulation[1] end - modulation = UTILS.GetModulationName(modulation) - local switchtext = self.gettext:GetEntry("BROADCAST",self.locale) - local playername = EventData.IniPlayerName - if EventData.IniGroup then - -- personalized flight name in player naming - if self.customcallsigns[playername] then - self.customcallsigns[playername] = nil + if self.UseSRS then + local frequency = self.Frequency + local freqtext = "" + if type(frequency) == "table" then + freqtext = self.gettext:GetEntry("FREQUENCIES",self.locale) + freqtext = freqtext..table.concat(frequency,", ") + else + local freqt = self.gettext:GetEntry("FREQUENCY",self.locale) + freqtext = string.format(freqt,frequency) end - playername = EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber) + local modulation = self.Modulation + if type(modulation) == "table" then modulation = modulation[1] end + modulation = UTILS.GetModulationName(modulation) + local switchtext = self.gettext:GetEntry("BROADCAST",self.locale) + + local playername = EventData.IniPlayerName + if EventData.IniGroup then + -- personalized flight name in player naming + if self.customcallsigns[playername] then + self.customcallsigns[playername] = nil + end + playername = EventData.IniGroup:GetCustomCallSign(self.ShortCallsign,self.Keepnumber) + end + playername = self:_GetTextForSpeech(playername) + --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext) + local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext) + self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation) end - playername = self:_GetTextForSpeech(playername) - --local text = string.format("%s, %s, switch to %s for task assignment!",EventData.IniPlayerName,self.MenuName or self.Name,freqtext) - local text = string.format(switchtext,playername,self.MenuName or self.Name,freqtext) - self.SRSQueue:NewTransmission(text,nil,self.SRS,timer.getAbsTime()+60,2,{EventData.IniGroup},text,30,self.BCFrequency,self.BCModulation) if EventData.IniPlayerName then self.PlayerMenu[EventData.IniPlayerName] = nil - --self:_BuildMenus(CLIENT:FindByName(EventData.IniUnitName)) - self:_BuildMenus(CLIENT:FindByPlayerName(EventData.IniPlayerName)) + local player = CLIENT:FindByName(EventData.IniUnitName) + self:_SwitchMenuForClient(player,"Info") end end end @@ -2308,7 +2347,7 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType() self:T(self.lid.."_GetTasksPerType") local tasktypes = self:_GetAvailableTaskTypes() - --self:T({tasktypes}) + --self:I({tasktypes}) -- Sort tasks per threat level first local datatable = self.TaskQueue:GetDataTable() @@ -2327,11 +2366,23 @@ function PLAYERTASKCONTROLLER:_GetTasksPerType() local threat=_data.threat local task = _data.task -- Ops.PlayerTask#PLAYERTASK local type = task.Type + local name = task.Target:GetName() + --self:I(name) if not task:IsDone() then + --self:I(name) table.insert(tasktypes[type],task) end end + --[[ + for _type,_data in pairs(tasktypes) do + self:I("Task Type: ".._type) + for _id,_task in pairs(_data) do + self:I("Task Name: ".._task.Target:GetName()) + end + end + --]] + return tasktypes end @@ -2443,7 +2494,7 @@ function PLAYERTASKCONTROLLER:_CheckTaskQueue() local client = _client --Wrapper.Client#CLIENT local group = client:GetGroup() for _,task in pairs(nexttasks) do - self:_JoinTask(group,client,task,true) + self:_JoinTask(task,true,group,client) end end end @@ -2462,7 +2513,7 @@ end -- @param #PLAYERTASKCONTROLLER self -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:_CheckPrecisionTasks() - self:T(self.lid.."_CheckTaskQueue") + self:T(self.lid.."_CheckPrecisionTasks") if self.PrecisionTasks:Count() > 0 and self.precisionbombing then if not self.LasingDrone or self.LasingDrone:IsDead() then -- we need a new drone @@ -2965,15 +3016,20 @@ end --- [Internal] Join a player to a task -- @param #PLAYERTASKCONTROLLER self --- @param Wrapper.Group#GROUP Group --- @param Wrapper.Client#CLIENT Client -- @param Ops.PlayerTask#PLAYERTASK Task -- @param #boolean Force Assign task even if client already has one +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client -- @return #PLAYERTASKCONTROLLER self -function PLAYERTASKCONTROLLER:_JoinTask(Group, Client, Task, Force) +function PLAYERTASKCONTROLLER:_JoinTask(Task, Force, Group, Client) + self:T({Force, Group, Client}) self:T(self.lid.."_JoinTask") + local force = false + if type(Force) == "boolean" then + force = Force + end local playername, ttsplayername = self:_GetPlayerName(Client) - if self.TasksPerPlayer:HasUniqueID(playername) and not Force then + if self.TasksPerPlayer:HasUniqueID(playername) and not force then -- Player already has a task if not self.NoScreenOutput then local text = self.gettext:GetEntry("HAVEACTIVETASK",self.locale) @@ -3004,7 +3060,7 @@ function PLAYERTASKCONTROLLER:_JoinTask(Group, Client, Task, Force) self.TasksPerPlayer:Push(Task,playername) self:__PlayerJoinedTask(1, Group, Client, Task) -- clear menu - self:_BuildMenus(Client,true) + self:_SwitchMenuForClient(Client,"Active",1) end if Task.Type == AUFTRAG.Type.PRECISIONBOMBING then if not self.PrecisionTasks:HasUniqueID(Task.PlayerTaskNr) then @@ -3065,19 +3121,23 @@ end --- [Internal] Show active task info -- @param #PLAYERTASKCONTROLLER self +-- @param Ops.PlayerTask#PLAYERTASK Task -- @param Wrapper.Group#GROUP Group -- @param Wrapper.Client#CLIENT Client --- @param Ops.PlayerTask#PLAYERTASK Task -- @return #PLAYERTASKCONTROLLER self -function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Group, Client, Task) +function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Task, Group, Client) self:T(self.lid.."_ActiveTaskInfo") local playername, ttsplayername = self:_GetPlayerName(Client) local text = "" local textTTS = "" - if self.TasksPerPlayer:HasUniqueID(playername) or Task then + local task = nil + if type(Task) ~= "string" then + task = Task + end + if self.TasksPerPlayer:HasUniqueID(playername) or task then -- NODO: Show multiple? -- Details - local task = Task or self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + local task = task or self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK local tname = self.gettext:GetEntry("TASKNAME",self.locale) local ttsname = self.gettext:GetEntry("TASKNAMETTS",self.locale) local taskname = string.format(tname,task.Type,task.PlayerTaskNr) @@ -3389,280 +3449,287 @@ function PLAYERTASKCONTROLLER:_AbortTask(Group, Client) if not self.NoScreenOutput then local m=MESSAGE:New(text,15,"Tasking"):ToClient(Client) end - self:_BuildMenus(Client,true) + self:_SwitchMenuForClient(Client,"Info",1) return self end ---- [Internal] Build Task Info Menu + +-- TODO - New Menu Manager +--- [Internal] _UpdateJoinMenuTemplate -- @param #PLAYERTASKCONTROLLER self --- @param Wrapper.Group#GROUP group --- @param Wrapper.Client#CLIENT client --- @param #string playername --- @param Core.Menu#MENU_BASE topmenu --- @param #table tasktypes --- @param #table taskpertype --- @param #string newtag --- @return #table taskinfomenu -function PLAYERTASKCONTROLLER:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) - self:T(self.lid.."_BuildTaskInfoMenu") - local taskinfomenu = nil - if self.taskinfomenu then - local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) - local taskinfomenu = MENU_GROUP:New(group,menutaskinfo,topmenu):SetTag(newtag) - local ittypes = {} - local itaskmenu = {} - local tnow = timer.getTime() - - for _tasktype,_data in pairs(tasktypes) do - ittypes[_tasktype] = MENU_GROUP:New(group,_tasktype,taskinfomenu):SetTag(newtag) - local tasks = taskpertype[_tasktype] or {} - local n = 0 - for _,_task in pairs(tasks) do - _task = _task -- Ops.PlayerTask#PLAYERTASK - local pilotcount = _task:CountClients() - local newtext = "]" - -- marker for new tasks - if tnow - _task.timestamp < 60 then - newtext = "*]" +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_UpdateJoinMenuTemplate() + self:T("_UpdateJoinMenuTemplate") + if self.TaskQueue:Count() > 0 then + local taskpertype = self:_GetTasksPerType() + local JoinMenu = self.JoinMenu -- Core.ClientMenu#CLIENTMENU + --self:I(JoinMenu.UUID) + local controller = self.JoinTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER + local actcontroller = self.ActiveTaskMenuTemplate -- Core.ClientMenu#CLIENTMENUMANAGER + local actinfomenu = self.ActiveInfoMenu + --local entrynumbers = {} + --local existingentries = {} + local maxn = self.menuitemlimit + -- Generate task type menu items + for _type,_ in pairs(taskpertype) do + local found = controller:FindEntriesByText(_type) + --self:I({found}) + if #found == 0 then + local newentry = controller:NewEntry(_type,JoinMenu) + controller:AddEntry(newentry) + if self.JoinInfoMenu then + local newentry = controller:NewEntry(_type,self.JoinInfoMenu) + controller:AddEntry(newentry) end - local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) - local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) - if self.UseGroupNames then - local name = _task.Target:GetName() - if name ~= "Unknown" then - text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) - end + if actinfomenu then + local newentry = actcontroller:NewEntry(_type,self.ActiveInfoMenu) + actcontroller:AddEntry(newentry) end - if self.UseTypeNames then - if _task.TypeName then - --local name = self.gettext:GetEntry(_task.TypeName,self.locale) - text = string.format("%s (%03d) [%d%s",_task.TypeName,_task.PlayerTaskNr,pilotcount,newtext) - --self:T(self.lid.."Menu text = "..text) - end - end - local taskentry = MENU_GROUP_COMMAND:New(group,text,ittypes[_tasktype],self._ActiveTaskInfo,self,group,client,_task):SetTag(newtag) - --taskentry:SetTag(playername) - itaskmenu[#itaskmenu+1] = taskentry - -- keep max items limit - n = n + 1 - if n >= self.menuitemlimit then - break - end end end + + local typelist = self:_GetAvailableTaskTypes() + -- Slot in Tasks + for _tasktype,_data in pairs(typelist) do + self:T("**** Building for TaskType: ".._tasktype) + --local tasks = taskpertype[_tasktype] or {} + for _,_task in pairs(taskpertype[_tasktype]) do + _task = _task -- Ops.PlayerTask#PLAYERTASK + self:T("**** Building for Task: ".._task.Target:GetName()) + if _task.InMenu then + self:T("**** Task already in Menu ".._task.Target:GetName()) + else + --local pilotcount = _task:CountClients() + --local newtext = "]" + --local tnow = timer.getTime() + -- marker for new tasks + --if tnow - _task.timestamp < 60 then + --newtext = "*]" + --end + local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) + --local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) + local text = string.format("%s %03d",menutaskno,_task.PlayerTaskNr) + if self.UseGroupNames then + local name = _task.Target:GetName() + if name ~= "Unknown" then + --text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) + text = string.format("%s (%03d)",name,_task.PlayerTaskNr) + end + end + --local taskentry = MENU_GROUP_COMMAND:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task):SetTag(newtag) + local parenttable, number = controller:FindEntriesByText(_tasktype,JoinMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = controller:FindEntriesByParent(Parent) + self:T("***** Join Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = controller:NewEntry(text,Parent,self._JoinTask,self,_task,"false") + controller:AddEntry(taskentry) + _task.InMenu = true + if not _task.UUIDS then _task.UUIDS = {} end + table.insert(_task.UUIDS,taskentry.UUID) + end + end + if self.JoinInfoMenu then + local parenttable, number = controller:FindEntriesByText(_tasktype,self.JoinInfoMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = controller:FindEntriesByParent(Parent) + self:T("***** Join Info Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = controller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task) + controller:AddEntry(taskentry) + _task.InMenu = true + if not _task.UUIDS then _task.UUIDS = {} end + table.insert(_task.UUIDS,taskentry.UUID) + end + end + end + if actinfomenu then + local parenttable, number = actcontroller:FindEntriesByText(_tasktype,self.ActiveInfoMenu) + if number > 0 then + local Parent = parenttable[1] + local matches, count = actcontroller:FindEntriesByParent(Parent) + self:T("***** Active Info Menu ".._tasktype.. " # of entries: "..count) + if count < self.menuitemlimit then + local taskentry = actcontroller:NewEntry(text,Parent,self._ActiveTaskInfo,self,_task) + actcontroller:AddEntry(taskentry) + _task.InMenu = true + if not _task.AUUIDS then _task.AUUIDS = {} end + table.insert(_task.AUUIDS,taskentry.UUID) + end + end + end + end + end + end end - return taskinfomenu + return self end ---- [Internal] Build client menus +--- [Internal] _RemoveMenuEntriesForTask -- @param #PLAYERTASKCONTROLLER self --- @param Wrapper.Client#CLIENT Client (optional) build for this client name only --- @param #boolean enforced --- @param #boolean fromsuccess +-- @param #PLAYERTASK Task +-- @param Wrapper.Client#CLIENT Client -- @return #PLAYERTASKCONTROLLER self -function PLAYERTASKCONTROLLER:_BuildMenus(Client,enforced,fromsuccess) - self:T(self.lid.."_BuildMenus") - - if self.MenuBuildLocked and (timer.getAbsTime() - self.MenuBuildLocked < 2) then - self:ScheduleOnce(2,self._BuildMenus,self,Client,enforced,fromsuccess) - return self - else - self.MenuBuildLocked = timer.getAbsTime() - end - - local clients = self.ClientSet:GetAliveSet() - local joinorabort = false - local timedbuild = false - - if Client then - -- client + enforced -- join task or abort - clients = {Client} - enforced = true - joinorabort = true - end - - local tasktypes = self:_GetAvailableTaskTypes() - local taskpertype = self:_GetTasksPerType() - - for _,_client in pairs(clients) do - if _client and _client:IsAlive() then - local client = _client -- Wrapper.Client#CLIENT - local group = client:GetGroup() - local unknown = self.gettext:GetEntry("UNKNOWN",self.locale) - local playername = client:GetPlayerName() or unknown - - local oldtag = self.PlayerMenuTag[playername] - local newtag = playername..timer.getAbsTime() - self.PlayerMenuTag[playername] = newtag - - if group and client then - --- - -- TOPMENU - --- - local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) - local longname = self.Name..taskings..self.Type - local menuname = self.MenuName or longname - local playerhastask = false - - if self:_CheckPlayerHasTask(playername) and not fromsuccess then playerhastask = true end - local topmenu = nil - --local oldmenu = nil - local rebuilddone = false - - self:T("Playerhastask = "..tostring(playerhastask).." Enforced = "..tostring(enforced).." Join or Abort = "..tostring(joinorabort)) - - -- Cases to rebuild menu - -- 1) new player - -- 2) player joined a task, joinorabort = true - -- 3) player left a task, joinorabort = true - -- 4) player has no task, but number of tasks changed, and last build > 30 secs ago - if self.PlayerMenu[playername] then - -- NOT a new player - -- 2)+3) Join or abort? - if joinorabort then - self.PlayerMenu[playername]:RemoveSubMenus() - self.PlayerMenu[playername] = MENU_GROUP:New(group,menuname,self.MenuParent) - self.PlayerMenu[playername]:SetTag(newtag) - topmenu = self.PlayerMenu[playername] - elseif (not playerhastask) or enforced then - -- 4) last build > 30 secs? - local T0 = timer.getAbsTime() - local TDiff = T0-self.PlayerMenu[playername].PTTimeStamp - self:T("TDiff = "..string.format("%.2d",TDiff)) - if TDiff >= self.holdmenutime then - --self.PlayerMenu[playername]:RemoveSubMenus() - --oldmenu = self.PlayerMenu[playername] - --self.PlayerMenu[playername] = nil - - --self.PlayerMenu[playername]:RemoveSubMenus() - --self.PlayerMenu[playername] = MENU_GROUP:New(group,menuname,self.MenuParent) - --self.PlayerMenu[playername]:SetTag(newtag) - --self.PlayerMenu[playername].PTTimeStamp = timer.getAbsTime() - --timedbuild = true - end - topmenu = self.PlayerMenu[playername] - end - else - -- 1) new player# - topmenu = MENU_GROUP:New(group,menuname,self.MenuParent) - self.PlayerMenu[playername] = topmenu - self.PlayerMenu[playername]:SetTag(newtag) - self.PlayerMenu[playername].PTTimeStamp = timer.getAbsTime() - enforced = true +function PLAYERTASKCONTROLLER:_RemoveMenuEntriesForTask(Task,Client) + self:T("_RemoveMenuEntriesForTask") + --self:I("Task name: "..Task.Target:GetName()) + --self:I("Client: "..Client:GetPlayerName()) + if Task then + if Task.UUIDS and self.JoinTaskMenuTemplate then + --self:I("***** JoinTaskMenuTemplate") + UTILS.PrintTableToLog(Task.UUIDS) + local controller = self.JoinTaskMenuTemplate + for _,_uuid in pairs(Task.UUIDS) do + local Entry = controller:FindEntryByUUID(_uuid) + if Entry then + controller:DeleteF10Entry(Entry,Client) + controller:DeleteGenericEntry(Entry) + UTILS.PrintTableToLog(controller.menutree) end - - --- - -- ACTIVE TASK MENU - --- - if playerhastask and enforced then - self:T("Building Active Task Menus for "..playername) - rebuilddone = true - local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale) - local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale) - local menumark = self.gettext:GetEntry("MENUMARK",self.locale) - local menusmoke = self.gettext:GetEntry("MENUSMOKE",self.locale) - local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale) - local menuabort = self.gettext:GetEntry("MENUABORT",self.locale) - - local active = MENU_GROUP:New(group,menuactive,topmenu):SetTag(newtag) - local info = MENU_GROUP_COMMAND:New(group,menuinfo,active,self._ActiveTaskInfo,self,group,client):SetTag(newtag) - local mark = MENU_GROUP_COMMAND:New(group,menumark,active,self._MarkTask,self,group,client):SetTag(newtag) - if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A then - if self.noflaresmokemenu ~= true then - -- no smoking/flaring here if A2A or designer has set noflaresmokemenu to true - local smoke = MENU_GROUP_COMMAND:New(group,menusmoke,active,self._SmokeTask,self,group,client):SetTag(newtag) - local flare = MENU_GROUP_COMMAND:New(group,menuflare,active,self._FlareTask,self,group,client):SetTag(newtag) - local IsNight = client:GetCoordinate():IsNight() - if IsNight then - local light = MENU_GROUP_COMMAND:New(group,menuflare,active,self._IlluminateTask,self,group,client):SetTag(newtag) - end - end - end - local abort = MENU_GROUP_COMMAND:New(group,menuabort,active,self._AbortTask,self,group,client):SetTag(newtag) - if self.activehasinfomenu and self.taskinfomenu then - self:T("Building Active-Info Menus for "..playername) - if self.PlayerInfoMenu[playername] then - self.PlayerInfoMenu[playername]:RemoveSubMenus(nil,oldtag) - end - self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) - end - elseif (self.TaskQueue:Count() > 0 and enforced) or (not playerhastask and (timedbuild or joinorabort)) then - self:T("Building Join Menus for "..playername) - rebuilddone = true - --- - -- JOIN TASK MENU - --- - local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale) - - if self.PlayerJoinMenu[playername] then - self.PlayerJoinMenu[playername]:RemoveSubMenus(nil,oldtag) - end - - local joinmenu = MENU_GROUP:New(group,menujoin,topmenu):SetTag(newtag) - self.PlayerJoinMenu[playername] = joinmenu - - local ttypes = {} - local taskmenu = {} - for _tasktype,_data in pairs(tasktypes) do - ttypes[_tasktype] = MENU_GROUP:New(group,_tasktype,joinmenu):SetTag(newtag) - local tasks = taskpertype[_tasktype] or {} - local n = 0 - for _,_task in pairs(tasks) do - _task = _task -- Ops.PlayerTask#PLAYERTASK - local pilotcount = _task:CountClients() - local newtext = "]" - local tnow = timer.getTime() - -- marker for new tasks - if tnow - _task.timestamp < 60 then - newtext = "*]" - end - local menutaskno = self.gettext:GetEntry("MENUTASKNO",self.locale) - local text = string.format("%s %03d [%d%s",menutaskno,_task.PlayerTaskNr,pilotcount,newtext) - if self.UseGroupNames then - local name = _task.Target:GetName() - if name ~= "Unknown" then - text = string.format("%s (%03d) [%d%s",name,_task.PlayerTaskNr,pilotcount,newtext) - end - end - local taskentry = MENU_GROUP_COMMAND:New(group,text,ttypes[_tasktype],self._JoinTask,self,group,client,_task):SetTag(newtag) - --taskentry:SetTag(playername) - taskmenu[#taskmenu+1] = taskentry - n = n + 1 - if n >= self.menuitemlimit then - break - end - end - end - if self.taskinfomenu then - self:T("Building Join-Info Menus for "..playername) - if self.PlayerInfoMenu[playername] then - self.PlayerInfoMenu[playername]:RemoveSubMenus(nil,oldtag) - end - self.PlayerInfoMenu[playername] = self:_BuildTaskInfoMenu(group,client,playername,topmenu,tasktypes,taskpertype,newtag) - end - end - if self.AllowFlash and topmenu ~= nil then - local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) - local flashmenu = MENU_GROUP_COMMAND:New(group,flashtext,topmenu,self._SwitchFlashing,self,group,client):SetTag(newtag) - end - if self.TaskQueue:Count() == 0 then - self:T("No open tasks info") - local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) - local joinmenu = MENU_GROUP:New(group,menunotasks,self.PlayerMenu[playername]):SetTag(newtag) - rebuilddone = true - end - --- - -- REFRESH MENU - --- - if rebuilddone then - --self.PlayerMenu[playername]:RemoveSubMenus(nil,oldtag) - --self.PlayerMenu[playername]:Set() - self.PlayerMenu[playername]:Refresh() - end end end + + if Task.AUUIDS and self.ActiveTaskMenuTemplate then + --self:I("***** ActiveTaskMenuTemplate") + UTILS.PrintTableToLog(Task.AUUIDS) + for _,_uuid in pairs(Task.AUUIDS) do + local controller = self.ActiveTaskMenuTemplate + local Entry = controller:FindEntryByUUID(_uuid) + if Entry then + controller:DeleteF10Entry(Entry,Client) + controller:DeleteGenericEntry(Entry) + UTILS.PrintTableToLog(controller.menutree) + end + end + end + + Task.UUIDS = nil + Task.AUUIDS = nil + end + return self +end + +--- [Internal] _CreateJoinMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CreateJoinMenuTemplate() + self:T("_CreateActiveTaskMenuTemplate") + + local menujoin = self.gettext:GetEntry("MENUJOIN",self.locale) + local menunotasks = self.gettext:GetEntry("MENUNOTASKS",self.locale) + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + + local JoinTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ClientSet,"JoinTask") + + if not self.JoinTopMenu then + local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) + local longname = self.Name..taskings..self.Type + local menuname = self.MenuName or longname + self.JoinTopMenu = JoinTaskMenuTemplate:NewEntry(menuname,self.MenuParent) + end + + if self.AllowFlash then + JoinTaskMenuTemplate:NewEntry(flashtext,self.JoinTopMenu,self._SwitchFlashing,self) + end + + self.JoinMenu = JoinTaskMenuTemplate:NewEntry(menujoin,self.JoinTopMenu) + + if self.taskinfomenu then + local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) + self.JoinInfoMenu = JoinTaskMenuTemplate:NewEntry(menutaskinfo,self.JoinTopMenu) + end + + if self.TaskQueue:Count() == 0 then + JoinTaskMenuTemplate:NewEntry(menunotasks,self.JoinMenu) + end + + self.JoinTaskMenuTemplate = JoinTaskMenuTemplate + + return self +end + +--- [Internal] _CreateActiveTaskMenuTemplate +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CreateActiveTaskMenuTemplate() + self:T("_CreateActiveTaskMenuTemplate") + + local menuactive = self.gettext:GetEntry("MENUACTIVE",self.locale) + local menuinfo = self.gettext:GetEntry("MENUINFO",self.locale) + local menumark = self.gettext:GetEntry("MENUMARK",self.locale) + local menusmoke = self.gettext:GetEntry("MENUSMOKE",self.locale) + local menuflare = self.gettext:GetEntry("MENUFLARE",self.locale) + local menuillu = self.gettext:GetEntry("MENUILLU",self.locale) + local menuabort = self.gettext:GetEntry("MENUABORT",self.locale) + + local ActiveTaskMenuTemplate = CLIENTMENUMANAGER:New(self.ActiveClientSet,"ActiveTask") + + if not self.ActiveTopMenu then + local taskings = self.gettext:GetEntry("MENUTASKING",self.locale) + local longname = self.Name..taskings..self.Type + local menuname = self.MenuName or longname + self.ActiveTopMenu = ActiveTaskMenuTemplate:NewEntry(menuname,self.MenuParent) + end + + if self.AllowFlash then + local flashtext = self.gettext:GetEntry("FLASHMENU",self.locale) + ActiveTaskMenuTemplate:NewEntry(flashtext,self.ActiveTopMenu,self._SwitchFlashing,self) + end + + local active = ActiveTaskMenuTemplate:NewEntry(menuactive,self.ActiveTopMenu) + ActiveTaskMenuTemplate:NewEntry(menuinfo,active,self._ActiveTaskInfo,self,"NONE") + ActiveTaskMenuTemplate:NewEntry(menumark,active,self._MarkTask,self) + + if self.Type ~= PLAYERTASKCONTROLLER.Type.A2A and self.noflaresmokemenu ~= true then + ActiveTaskMenuTemplate:NewEntry(menusmoke,active,self._SmokeTask,self) + ActiveTaskMenuTemplate:NewEntry(menuflare,active,self._FlareTask,self) + + if self.illumenu then + ActiveTaskMenuTemplate:NewEntry(menuillu,active,self._IlluminateTask,self) + end + + end + + ActiveTaskMenuTemplate:NewEntry(menuabort,active,self._AbortTask,self) + self.ActiveTaskMenuTemplate = ActiveTaskMenuTemplate + + if self.taskinfomenu and self.activehasinfomenu then + local menutaskinfo = self.gettext:GetEntry("MENUTASKINFO",self.locale) + self.ActiveInfoMenu = ActiveTaskMenuTemplate:NewEntry(menutaskinfo,self.ActiveTopMenu) + end + + return self +end + +--- [Internal] _SwitchMenuForClient +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Client#CLIENT Client The client +-- @param #string MenuType +-- @param #number Delay +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SwitchMenuForClient(Client,MenuType,Delay) + self:T(self.lid.."_SwitchMenuForClient") + if Delay then + self:ScheduleOnce(Delay,self._SwitchMenuForClient,self,Client,MenuType) + return self + end + if MenuType == "Info" then + self.ClientSet:AddClientsByName(Client:GetName()) + self.ActiveClientSet:Remove(Client:GetName(),true) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:Propagate(Client) + elseif MenuType == "Active" then + self.ActiveClientSet:AddClientsByName(Client:GetName()) + self.ClientSet:Remove(Client:GetName(),true) + self.ActiveTaskMenuTemplate:ResetMenu(Client) + self.JoinTaskMenuTemplate:ResetMenu(Client) + self.ActiveTaskMenuTemplate:Propagate(Client) + else + self:E(self.lid .."Unknown menu type in _SwitchMenuForClient:"..tostring(MenuType)) end - self.MenuBuildLocked = false return self end @@ -3810,8 +3877,8 @@ end -- @param Core.Menu#MENU_MISSION Menu -- @return #PLAYERTASKCONTROLLER self function PLAYERTASKCONTROLLER:SetParentMenu(Menu) - self:T(self.lid.."SetParentName") - self.MenuParent = Menu + self:T(self.lid.."SetParentMenu") + --self.MenuParent = Menu return self end @@ -3965,6 +4032,20 @@ end -- TODO: FSM Functions PLAYERTASKCONTROLLER ------------------------------------------------------------------------------------------------------------------- +--- [Internal] On after start call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStart(From, Event, To) + self:T({From, Event, To}) + self:T(self.lid.."onafterStart") + self:_CreateJoinMenuTemplate() + self:_CreateActiveTaskMenuTemplate() + return self +end + --- [Internal] On after Status call -- @param #PLAYERTASKCONTROLLER self -- @param #string From @@ -3994,7 +4075,7 @@ function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To) end end - self:_BuildMenus(nil,enforcedmenu) + self:_UpdateJoinMenuTemplate() if self.verbose then local text = string.format("%s | New Targets: %02d | Active Tasks: %02d | Active Players: %02d | Assigned Tasks: %02d",self.MenuName, targetcount,taskcount,playercount,assignedtasks) @@ -4041,6 +4122,16 @@ function PLAYERTASKCONTROLLER:onafterTaskCancelled(From, Event, To, Task) taskname = string.format(canceltxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) end + + local clients=Task:GetClientObjects() + for _,client in pairs(clients) do + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + --self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) + end return self end @@ -4065,9 +4156,15 @@ function PLAYERTASKCONTROLLER:onafterTaskSuccess(From, Event, To, Task) taskname = string.format(succtxttts, self.MenuName or self.Name, Task.PlayerTaskNr, tostring(Task.TTSType)) self.SRSQueue:NewTransmission(taskname,nil,self.SRS,nil,2) end + local clients=Task:GetClientObjects() for _,client in pairs(clients) do - self:_BuildMenus(client,true,true) + self:_RemoveMenuEntriesForTask(Task,client) + --self:_SwitchMenuForClient(client,"Info") + end + for _,client in pairs(clients) do + -- self:_RemoveMenuEntriesForTask(Task,client) + self:_SwitchMenuForClient(client,"Info",5) end return self end diff --git a/Moose Development/Moose/Utilities/Enums.lua b/Moose Development/Moose/Utilities/Enums.lua index 6c47c5216..b00552dcf 100644 --- a/Moose Development/Moose/Utilities/Enums.lua +++ b/Moose Development/Moose/Utilities/Enums.lua @@ -236,6 +236,7 @@ ENUMS.WeaponType.Any={ -- @field #string ESCORT Escort another group. -- @field #string FIGHTERSWEEP Fighter sweep. -- @field #string GROUNDATTACK Ground attack. +-- @field #string GROUNDESCORT Ground escort another group. -- @field #string INTERCEPT Intercept. -- @field #string PINPOINTSTRIKE Pinpoint strike. -- @field #string RECONNAISSANCE Reconnaissance mission. @@ -251,6 +252,7 @@ ENUMS.MissionTask={ CAP="CAP", CAS="CAS", ESCORT="Escort", + GROUNDESCORT="Ground escort", FIGHTERSWEEP="Fighter Sweep", GROUNDATTACK="Ground Attack", INTERCEPT="Intercept", diff --git a/Moose Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index 891718da7..99c643a7a 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -299,14 +299,14 @@ end -- @param #table tbl Input table. UTILS.OneLineSerialize = function( tbl ) -- serialization of a table all on a single line, no comments, made to replace old get_table_string function - lookup_table = {} +local lookup_table = {} local function _Serialize( tbl ) if type(tbl) == 'table' then --function only works for tables! if lookup_table[tbl] then - return lookup_table[object] + return lookup_table[tbl] end local tbl_str = {} diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 66aca8d82..276ec5b8d 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -12,7 +12,7 @@ -- @image Wrapper_Airbase.JPG ---- @type AIRBASE +-- @type AIRBASE -- @field #string ClassName Name of the class, i.e. "AIRBASE". -- @field #table CategoryName Names of airbase categories. -- @field #string AirbaseName Name of the airbase. @@ -1501,16 +1501,16 @@ function AIRBASE:FindFreeParkingSpotForAircraft(group, terminaltype, scanradius, -- Get the aircraft size, i.e. it's longest side of x,z. local aircraft = nil -- fix local problem below - local _aircraftsize, ax,ay,az + -- SU27 dimensions as default + local _aircraftsize = 23 + local ax = 23 -- l + local ay = 7 -- h + local az = 17 -- w if group and group.ClassName == "GROUP" then aircraft=group:GetUnit(1) - _aircraftsize, ax,ay,az=aircraft:GetObjectSize() - else - -- SU27 dimensions - _aircraftsize = 23 - ax = 23 -- length - ay = 7 -- height - az = 17 -- width + if aircraft then + _aircraftsize, ax,ay,az=aircraft:GetObjectSize() + end end diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 07275bc2f..37067d492 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -52,6 +52,7 @@ -- * @{#CONTROLLABLE.TaskEmbarking}: (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable. -- * @{#CONTROLLABLE.TaskEmbarkToTransport}: (GROUND) Embark to a Transport landed at a location. -- * @{#CONTROLLABLE.TaskEscort}: (AIR) Escort another airborne controllable. +-- * @{#CONTROLLABLE.TaskGroundEscort}: (AIR/HELO) Escort a ground controllable. -- * @{#CONTROLLABLE.TaskFAC_AttackGroup}: (AIR + GROUND) The task makes the controllable/unit a FAC and orders the FAC to control the target (enemy ground controllable) destruction. -- * @{#CONTROLLABLE.TaskFireAtPoint}: (GROUND) Fire some or all ammunition at a VEC2 point. -- * @{#CONTROLLABLE.TaskFollow}: (AIR) Following another airborne controllable. @@ -1480,15 +1481,53 @@ function CONTROLLABLE:TaskFollow( FollowControllable, Vec3, LastWaypointIndex ) return DCSTask end +--- (AIR/HELO) Escort a ground controllable. +-- The unit / controllable will follow lead unit of the other controllable, additional units of both controllables will continue following their leaders. +-- The unit / controllable will also protect that controllable from threats of specified types. +-- @param #CONTROLLABLE self +-- @param #CONTROLLABLE FollowControllable The controllable to be escorted. +-- @param #number LastWaypointIndex (optional) Detach waypoint of another controllable. Once reached the unit / controllable Escort task is finished. +-- @param #number OrbitDistance (optional) Maximum distance helo will orbit around the ground unit in meters. Defaults to 2000 meters. +-- @param DCS#AttributeNameArray TargetTypes (optional) Array of AttributeName that is contains threat categories allowed to engage. Default {"Ground vehicles"}. See [https://wiki.hoggit.us/view/DCS_enum_attributes](https://wiki.hoggit.us/view/DCS_enum_attributes) +-- @return DCS#Task The DCS task structure. +function CONTROLLABLE:TaskGroundEscort( FollowControllable, LastWaypointIndex, OrbitDistance, TargetTypes ) + + -- Escort = { + -- id = 'GroundEscort', + -- params = { + -- groupId = Group.ID, -- must + -- engagementDistMax = Distance, -- Must. With his task it does not appear to actually define the range AI are allowed to attack at, rather it defines the size length of the orbit. The helicopters will fly up to this set distance before returning to the escorted group. + -- lastWptIndexFlag = boolean, -- optional + -- lastWptIndex = number, -- optional + -- targetTypes = array of AttributeName, -- must + -- lastWptIndexFlagChangedManually = boolean, -- must be true + -- } + -- } + + local DCSTask = { + id = 'GroundEscort', + params = { + groupId = FollowControllable and FollowControllable:GetID() or nil, + engagementDistMax = OrbitDistance or 2000, + lastWptIndexFlag = LastWaypointIndex and true or false, + lastWptIndex = LastWaypointIndex, + targetTypes = TargetTypes or {"Ground vehicles"}, + lastWptIndexFlagChangedManually = true, + }, + } + + return DCSTask +end + --- (AIR) Escort another airborne controllable. -- The unit / controllable will follow lead unit of another controllable, wingmens of both controllables will continue following their leaders. -- The unit / controllable will also protect that controllable from threats of specified types. -- @param #CONTROLLABLE self -- @param #CONTROLLABLE FollowControllable The controllable to be escorted. -- @param DCS#Vec3 Vec3 Position of the unit / lead unit of the controllable relative lead unit of another controllable in frame reference oriented by course of lead unit of another controllable. If another controllable is on land the unit / controllable will orbit around. --- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Follow task is finished. --- @param #number EngagementDistance Maximal distance from escorted controllable to threat. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. --- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. +-- @param #number LastWaypointIndex Detach waypoint of another controllable. Once reached the unit / controllable Escort task is finished. +-- @param #number EngagementDistance Maximal distance from escorted controllable to threat in meters. If the threat is already engaged by escort escort will disengage if the distance becomes greater than 1.5 * engagementDistMax. +-- @param DCS#AttributeNameArray TargetTypes Array of AttributeName that is contains threat categories allowed to engage. Default {"Air"}. See https://wiki.hoggit.us/view/DCS_enum_attributes -- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes ) @@ -1504,8 +1543,7 @@ function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, E -- } -- } - local DCSTask - DCSTask = { + local DCSTask = { id = 'Escort', params = { groupId = FollowControllable and FollowControllable:GetID() or nil, diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 6839b895e..0ad57277f 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -32,6 +32,7 @@ Core/Spot.lua Core/TextAndSound.lua Core/Condition.lua Core/Pathline.lua +Core/ClientMenu.lua Wrapper/Object.lua Wrapper/Identifiable.lua