diff --git a/Moose Development/Moose/Core/Menu.lua b/Moose Development/Moose/Core/Menu.lua index 58b385e60..54328a0ec 100644 --- a/Moose Development/Moose/Core/Menu.lua +++ b/Moose Development/Moose/Core/Menu.lua @@ -239,6 +239,7 @@ do -- MENU_BASE function MENU_BASE:GetMenu( MenuText ) return self.Menus[MenuText] end + --- Sets a menu stamp for later prevention of menu removal. -- @param #MENU_BASE self -- @param MenuStamp @@ -276,6 +277,7 @@ do -- MENU_BASE end end + do -- MENU_COMMAND_BASE --- @type MENU_COMMAND_BASE -- @field #function MenuCallHandler @@ -813,6 +815,7 @@ do end end + --- Refreshes a new radio item for a group and submenus -- @param #MENU_GROUP self -- @return #MENU_GROUP diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index ee276c71b..fea437e70 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -1152,8 +1152,10 @@ do -- SET_GROUP local NearestGroup = nil --Wrapper.Group#GROUP local ClosestDistance = nil - - for ObjectID, ObjectData in pairs( self.Set ) do + + local Set = self:GetAliveSet() + + for ObjectID, ObjectData in pairs( Set ) do if NearestGroup == nil then NearestGroup = ObjectData ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() ) @@ -5825,8 +5827,7 @@ do -- SET_ZONE if self.Filter.Prefixes then local MZonePrefix = false for ZonePrefixId, ZonePrefix in pairs( self.Filter.Prefixes ) do - env.info(string.format("zone %s %s", MZoneName, ZonePrefix)) - self:I( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) + self:T2( { "Prefix:", string.find( MZoneName, ZonePrefix, 1 ), ZonePrefix } ) if string.find( MZoneName, ZonePrefix, 1 ) then MZonePrefix = true end diff --git a/Moose Development/Moose/Functional/Autolase.lua b/Moose Development/Moose/Functional/Autolase.lua index b67313f44..daffdfa22 100644 --- a/Moose Development/Moose/Functional/Autolase.lua +++ b/Moose Development/Moose/Functional/Autolase.lua @@ -106,10 +106,11 @@ AUTOLASE = { -- @field #string unitname -- @field #string reccename -- @field #string unittype +-- @field Core.Point#COORDINATE coordinate --- AUTOLASE class version. -- @field #string version -AUTOLASE.version = "0.0.11" +AUTOLASE.version = "0.0.12" ------------------------------------------------------------------- -- Begin Functional.Autolase.lua @@ -578,6 +579,17 @@ function AUTOLASE:ShowStatus(Group) local typename = entry.unittype local code = entry.lasercode local locationstring = entry.location + local playername = Group:GetPlayerName() + if playername then + local settings = _DATABASE:GetPlayerSettings(playername) + if settings then + if settings:IsA2G_MGRS() then + locationstring = entry.coordinate:ToStringMGRS(settings) + elseif settings:IsA2G_LL_DMS() then + locationstring = entry.coordinate:ToStringLLDMS() + end + end + end local text = string.format("%s lasing %s code %d\nat %s",reccename,typename,code,locationstring) report:Add(text) lines = lines + 1 @@ -832,7 +844,16 @@ function AUTOLASE:onafterMonitor(From, Event, To) local code = self:GetLaserCode(reccename) local spot = SPOT:New(recce) spot:LaseOn(unit,code,self.LaseDuration) - local locationstring = unit:GetCoordinate():ToStringLLDDM() + local locationstring = unit:GetCoordinate():ToStringLLDDM() + if _SETTINGS:IsA2G_MGRS() then + local precision = _SETTINGS:GetMGRS_Accuracy() + local settings = {} + settings.MGRS_Accuracy = precision + locationstring = unit:GetCoordinate():ToStringMGRS(settings) + elseif _SETTINGS:IsA2G_LL_DMS() then + locationstring = unit:GetCoordinate():ToStringLLDMS() + end + local laserspot = { -- #AUTOLASE.LaserSpot laserspot = spot, lasedunit = unit, @@ -843,6 +864,7 @@ function AUTOLASE:onafterMonitor(From, Event, To) unitname = unitname, reccename = reccename, unittype = unit:GetTypeName(), + coordinate = unit:GetCoordinate(), } if self.smoketargets then local coord = unit:GetCoordinate() diff --git a/Moose Development/Moose/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 231a964c8..89330344e 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -2049,9 +2049,15 @@ function RAT:_InitAircraft(DCSgroup) --self.aircraft.descriptors=DCSdesc -- aircraft dimensions - self.aircraft.length=DCSdesc.box.max.x - self.aircraft.height=DCSdesc.box.max.y - self.aircraft.width=DCSdesc.box.max.z + if DCSdesc.box then + self.aircraft.length=DCSdesc.box.max.x + self.aircraft.height=DCSdesc.box.max.y + self.aircraft.width=DCSdesc.box.max.z + elseif DCStype == "Mirage-F1CE" then + self.aircraft.length=16 + self.aircraft.height=5 + self.aircraft.width=9 + end self.aircraft.box=math.max(self.aircraft.length,self.aircraft.width) -- info message diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 9928e2e59..ab535cc79 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -104,6 +104,7 @@ __Moose.Include( 'Scripts/Moose/Ops/Fleet.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Awacs.lua' ) __Moose.Include( 'Scripts/Moose/Ops/Operation.lua' ) __Moose.Include( 'Scripts/Moose/Ops/FlightControl.lua' ) +__Moose.Include( 'Scripts/Moose/Ops/PlayerTask.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Balancer.lua' ) __Moose.Include( 'Scripts/Moose/AI/AI_Air.lua' ) diff --git a/Moose Development/Moose/Ops/PlayerTask.lua b/Moose Development/Moose/Ops/PlayerTask.lua new file mode 100644 index 000000000..e3fbf16d2 --- /dev/null +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -0,0 +1,1115 @@ +--- **Ops** - PlayerTask (mission) for Players. +-- +-- ## Main Features: +-- +-- * Simplifies defining and executing Player tasks +-- * FSM events when a mission is done, successful or failed +-- +-- === +-- +-- ## Example Missions: +-- +-- Demo missions can be found on [github](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/). +-- +-- === +-- +-- ### Author: **Applevangelist** +-- +-- === +-- @module Ops.PlayerTask +-- @image OPS_PlayerTask.png + + +--- PLAYERTASK class. +-- @type PLAYERTASK +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field #number PlayerTaskNr (Globally unique) Number of the task. +-- @field Ops.Auftrag#AUFTRAG.Type Type The type of the task +-- @field Ops.Target#TARGET Target The target for this Task +-- @field Utilities.FiFo#FIFO Clients FiFo of Wrapper.Client#CLIENT planes executing this task +-- @field #boolean Repeat +-- @field #number repeats +-- @field #number RepeatNo +-- @field Wrapper.Marker#MARKER TargetMarker +-- @field #number SmokeColor +-- @field #number FlareColor +-- @field #table conditionSuccess = {}, +-- @field #table conditionFailure = {}, +-- @field Ops.PlayerTask#PLAYERTASKCONTROLLER TaskController +-- +-- @extends Core.Fsm#FSM + +--- Global PlayerTaskNr counter +_PlayerTaskNr = 0 + +--- +-- @field #PLAYERTASK +PLAYERTASK = { + ClassName = "PLAYERTASK", + verbose = true, + lid = nil, + PlayerTaskNr = nil, + Type = nil, + Target = nil, + Clients = nil, + Repeat = false, + repeats = 0, + RepeatNo = 1, + TargetMarker = nil, + SmokeColor = nil, + FlareColor = nil, + conditionSuccess = {}, + conditionFailure = {}, + TaskController = nil, + } + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASK.version="0.0.6" + +--- Generic task condition. +-- @type PLAYERTASK.Condition +-- @field #function func Callback function to check for a condition. Should return a #boolean. +-- @field #table arg Optional arguments passed to the condition callback function. + +--- Constructor +-- @param #PLAYERTASK self +-- @param Ops.Auftrag#AUFTRAG.Type Type Type of this task +-- @param Ops.Target#TARGET Target Target for this task +-- @param #boolean Repeat Repeat this task if true (default = false) +-- @param #number Times Repeat on failure this many times if Repeat is true (default = 1) +-- @return #PLAYERTASK self +function PLAYERTASK:New(Type, Target, Repeat, Times) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASK + + self.Type = Type + + self.Repeat = false + self.repeats = 0 + self.RepeatNo = 1 + self.Clients = FIFO:New() -- Utilities.FiFo#FIFO + self.TargetMarker = nil -- Wrapper.Marker#MARKER + self.SmokeColor = SMOKECOLOR.Red + self.conditionSuccess = {} + self.conditionFailure = {} + self.TaskController = nil -- Ops.PlayerTask#PLAYERTASKCONTROLLER + + if Repeat then + self.Repeat = true + self.RepeatNo = Times or 1 + end + + _PlayerTaskNr = _PlayerTaskNr + 1 + + self.PlayerTaskNr = _PlayerTaskNr + + self.lid=string.format("PlayerTask #%d %s | ", self.PlayerTaskNr, tostring(self.Type)) + + if Target and Target.ClassName and Target.ClassName == "TARGET" then + self.Target = Target + elseif Target and Target.ClassName then + self.Target = TARGET:New(Target) + else + self:E(self.lid.."*** NO VALID TARGET!") + return self + end + + self:I(self.lid.."Created.") + + -- FMS start state is PLANNED. + self:SetStartState("Planned") + + -- PLANNED --> REQUESTED --> EXECUTING --> DONE + self:AddTransition("*", "Planned", "Planned") -- Task is in planning stage. + self:AddTransition("*", "Requested", "Requested") -- Task clients have been requested to join. + self:AddTransition("*", "ClientAdded", "*") -- Client has been added to the task + self:AddTransition("*", "ClientRemoved", "*") -- Client has been added to the task + self:AddTransition("*", "Executing", "Executing") -- First client is executing the Task. + self:AddTransition("*", "Done", "Done") -- All clients have reported that Task is done. + self:AddTransition("*", "Cancel", "Done") -- Command to cancel the Task. + self:AddTransition("*", "Success", "Done") + self:AddTransition("*", "ClientAborted", "*") + self:AddTransition("*", "Failed", "*") -- Done or repeat --> PLANNED + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Status(-5) + return self +end + +--- [Internal] Add a PLAYERTASKCONTROLLER for this task +-- @param #PLAYERTASK self +-- @param Ops.PlayerTask#PLAYERTASKCONTROLLER Controller +-- @return #PLAYERTASK self +function PLAYERTASK:_SetController(Controller) + self:I(self.lid.."_SetController") + self.TaskController = Controller + return self +end + +--- [User] Check is task is done +-- @param #PLAYERTASK self +-- @return #boolean done +function PLAYERTASK:IsDone() + self:I(self.lid.."IsDone?") + local IsDone = false + local state = self:GetState() + if state == "Done" or state == "Stopped" then + IsDone = true + end + return IsDone +end + +--- [User] Add a client to this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:AddClient(Client) + self:I(self.lid.."AddClient") + local name = Client:GetPlayerName() + if not self.Clients:HasUniqueID(name) then + self.Clients:Push(Client,name) + self:__ClientAdded(-2,Client) + end + return self +end + +--- [User] Remove a client from this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:RemoveClient(Client) + self:I(self.lid.."RemoveClient") + local name = Client:GetPlayerName() + if self.Clients:HasUniqueID(name) then + self.Clients:PullByID(name) + self:__ClientRemoved(-2,Client) + if self.Clients:Count() == 0 then + self:__Failed(-1) + end + end + return self +end + +--- [User] Client has aborted task this task +-- @param #PLAYERTASK self +-- @param Wrapper.Client#CLIENT Client (optional) +-- @return #PLAYERTASK self +function PLAYERTASK:ClientAbort(Client) + self:I(self.lid.."ClientAbort") + if Client and Client:IsAlive() then + self:RemoveClient(Client) + self:__ClientAborted(-1,Client) + return self + else + -- no client given, abort whole task if no one else is assigned + if self.Clients:Count() == 0 then + -- return to planned state if repeat + self:__Failed(-1) + end + end + return self +end + +--- [User] Create target mark on F10 map +-- @param #PLAYERTASK self +-- @return #PLAYERTASK self +function PLAYERTASK:MarkTargetOnF10Map() + self:I(self.lid.."MarkTargetOnF10Map") + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + if self.TargetMarker then + -- Marker exists, delete one first + self.TargetMarker:Remove() + end + self.TargetMarker = MARKER:New(coordinate,"Target of "..self.lid) + self.TargetMarker:ReadOnly() + self.TargetMarker:ToAll() + end + end + return self +end + +--- [User] Smoke Target +-- @param #PLAYERTASK self +-- @param #number Color, defaults to SMOKECOLOR.Red +-- @return #PLAYERTASK self +function PLAYERTASK:SmokeTarget(Color) + self:I(self.lid.."SmokeTarget") + local color = Color or SMOKECOLOR.Red + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + coordinate:Smoke(color) + end + end + return self +end + +--- [User] Flare Target +-- @param #PLAYERTASK self +-- @param #number Color, defaults to FLARECOLOR.Red +-- @return #PLAYERTASK self +function PLAYERTASK:FlareTarget(Color) + self:I(self.lid.."SmokeTarget") + local color = Color or FLARECOLOR.Red + if self.Target then + local coordinate = self.Target:GetCoordinate() + if coordinate then + coordinate:Flare(color,0) + end + end + return self +end + +-- success / failure function addion courtesy @FunkyFranky. + +--- [User] Add success condition. +-- @param #PLAYERTASK self +-- @param #function ConditionFunction If this function returns `true`, the mission is cancelled. +-- @param ... Condition function arguments if any. +-- @return #PLAYERTASK self +function PLAYERTASK:AddConditionSuccess(ConditionFunction, ...) + + local condition={} --#PLAYERTASK.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionSuccess, condition) + + return self +end + +--- [User] Add failure condition. +-- @param #PLAYERTASK self +-- @param #function ConditionFunction If this function returns `true`, the task is cancelled. +-- @param ... Condition function arguments if any. +-- @return #PLAYERTASK self +function PLAYERTASK:AddConditionFailure(ConditionFunction, ...) + + local condition={} --#PLAYERTASK.Condition + + condition.func=ConditionFunction + condition.arg={} + if arg then + condition.arg=arg + end + + table.insert(self.conditionFailure, condition) + + return self +end + +--- [Internal] Check if any of the given conditions is true. +-- @param #PLAYERTASK self +-- @param #table Conditions Table of conditions. +-- @return #boolean If true, at least one condition is true. +function PLAYERTASK:_EvalConditionsAny(Conditions) + + -- Any stop condition must be true. + for _,_condition in pairs(Conditions or {}) do + local condition=_condition --#AUFTRAG.Condition + + -- Call function. + local istrue=condition.func(unpack(condition.arg)) + + -- Any true will return true. + if istrue then + return true + end + + end + + -- No condition was true. + return false +end + +--- [Internal] On after status call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterStatus(From, Event, To) + self:I({From, Event, To}) + self:I(self.lid.."onafterStatus") + + local status = self:GetState() + + -- Check Target status + local targetdead = false + if self.Target:IsDead() or self.Target:IsDestroyed() then + targetdead = true + self:__Success(-2) + status = "Success" + return self + end + + if status == "Executing" then + -- Check Clients alive + local clientsalive = false + local ClientTable = self.Clients:GetDataTable() + for _,_client in pairs(ClientTable) do + local client = _client -- Wrapper.Client#CLIENT + if client:IsAlive() then + clientsalive=true -- one or more clients alive + end + end + + -- Failed? + if status == "Executing" and (not clientsalive) and (not targetdead) then + self:__Failed(-2) + status = "Failed" + end + + -- Any success condition true? + local successCondition=self:_EvalConditionsAny(self.conditionSuccess) + + -- Any failure condition true? + local failureCondition=self:_EvalConditionsAny(self.conditionFailure) + + if failureCondition then + self:__Failed(-2) + status = "Failed" + elseif successCondition then + self:__Success(-2) + status = "Success" + end + + if self.verbose then + self:I(self.lid.."Target dead: "..tostring(targetdead).." | Clients alive: " .. tostring(clientsalive)) + end + end + + -- Continue if we are not done + if status ~= "Done" then + self:__Status(-20) + else + self:__Stop(-1) + end + + return self +end + + +--- [Internal] On after planned call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterPlanned(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after requested call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterRequested(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after executing call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterExecuting(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after status call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterStop(From, Event, To) + self:I({From, Event, To}) + return self +end + +--- [Internal] On after client added call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASK self +function PLAYERTASK:onafterClientAdded(From, Event, To, Client) + self:I({From, Event, To}) + if Client then + local text = string.format("Player %s joined task %d!",Client:GetPlayerName() or "Generic",self.PlayerTaskNr) + self:I(self.lid..text) + end + return self +end + +--- [Internal] On after done call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterDone(From, Event, To) + self:I({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskDone(-1,self) + end + self:__Stop(-1) + return self +end + +--- [Internal] On after cancel call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterCancel(From, Event, To) + self:I({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskCancelled(-1,self) + end + self:__Done(-1) + return self +end + +--- [Internal] On after success call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterSuccess(From, Event, To) + self:I({From, Event, To}) + if self.TaskController then + self.TaskController:__TaskSuccess(-1,self) + end + if self.TargetMarker then + self.TargetMarker:Remove() + end + self:__Done(-1) + return self +end + +--- [Internal] On after failed call +-- @param #PLAYERTASK self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASK self +function PLAYERTASK:onafterFailed(From, Event, To) + self:I({From, Event, To}) + self.repeats = self.repeats + 1 + -- repeat on failed? + if self.Repeat and (self.repeats <= self.RepeatNo) then + if self.TaskController then + self.TaskController:__TaskRepeatOnFailed(-1,self) + end + self:__Planned(-1) + return self + else + if self.TargetMarker then + self.TargetMarker:Remove() + end + if self.TaskController then + self.TaskController:__TaskFailed(-1,self) + end + self:__Done(-1) + end + return self +end + +------------------------------------------------------------------------------------------------------------------- +-- PLAYERTASKCONTROLLER +-- TODO: PLAYERTASKCONTROLLER +------------------------------------------------------------------------------------------------------------------- + +--- PLAYERTASKCONTROLLER class. +-- @type PLAYERTASKCONTROLLER +-- @field #string ClassName Name of the class. +-- @field #boolean verbose Switch verbosity. +-- @field #string lid Class id string for output to DCS log file. +-- @field Utilities.FiFo#FIFO TargetQueue +-- @field Utilities.FiFo#FIFO TaskQueue +-- @field Utilities.FiFo#FIFO TasksPerPlayer +-- @field Core.Set#SET_CLIENT ClientSet +-- @field #string ClientFilter +-- @field #string Name +-- @field #string Type +-- @field #boolean UseGroupNames +-- + + +--- +-- @field #PLAYERTASKCONTROLLER +PLAYERTASKCONTROLLER = { + ClassName = "PLAYERTASKCONTROLLER", + verbose = true, + lid = nil, + TargetQueue = nil, + ClientSet = nil, + UseGroupNames = true, + } + +--- +-- @field Type +PLAYERTASKCONTROLLER.Type = { + A2A = "Air-To-Air", + A2G = "Air-To-Ground", + A2S = "Air-To-Sea", +} + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASKCONTROLLER.version="0.0.4" + +--- Constructor +-- @param #PLAYERTASKCONTROLLER self +-- @param #string Name Name of this controller +-- @param #number Coalition of this controller, e.g. coalition.side.BLUE +-- @param #string Type Type of the tasks controlled, defaults to PLAYERTASKCONTROLLER.Type.A2G +-- @param #string ClientFilter (optional) Additional prefix filter for the SET_CLIENT +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:New(Name, Coalition, Type, ClientFilter) + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, FSM:New()) -- #PLAYERTASKCONTROLLER + + self.Name = Name or "CentCom" + self.Coalition = Coalition or coalition.side.BLUE + self.CoalitionName = UTILS.GetCoalitionName(Coalition) + self.Type = Type or PLAYERTASKCONTROLLER.Type.A2G + self.ClientFilter = ClientFilter or "" + + self.TargetQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TaskQueue = FIFO:New() -- Utilities.FiFo#FIFO + self.TasksPerPlayer = FIFO:New() -- Utilities.FiFo#FIFO + + self.repeatonfailed = true + self.repeattimes = 5 + self.UseGroupNames = true + + if ClientFilter then + self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterPrefixes(ClientFilter):FilterStart() + else + self.ClientSet = SET_CLIENT:New():FilterCoalitions(string.lower(self.CoalitionName)):FilterActive(true):FilterStart() + end + + self.lid=string.format("PlayerTaskController %s %s | ", self.Name, tostring(self.Type)) + + -- FSM start state is STOPPED. + self:SetStartState("Stopped") + + self:AddTransition("Stopped", "Start", "Running") + self:AddTransition("*", "Status", "*") + self:AddTransition("*", "TaskDone", "*") + self:AddTransition("*", "TaskCancelled", "*") + self:AddTransition("*", "TaskSuccess", "*") + self:AddTransition("*", "TaskFailed", "*") + self:AddTransition("*", "TaskRepeatOnFailed", "*") + self:AddTransition("*", "Stop", "Stopped") + + self:__Start(-1) + self:__Status(-2) + + self:I(self.lid.."Started.") + + return self +end + +function PLAYERTASKCONTROLLER:_DummyMenu(group) + self:I(self.lid.."_DummyMenu") + return self +end + +--- [user] Switch usage of target names for menu entries on or off +-- @param #PLAYERTASKCONTROLLER self +-- @param #boolean OnOff If true, set to on (default), if nil or false, set to off +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:SwitchUseGroupNames(OnOff) + self:I(self.lid.."SwitchUseGroupNames") + if OnOff then + self.UseGroupNames = true + else + self.UseGroupNames = false + end + return self +end + +--- [Internal] Get task types for the menu +-- @param #PLAYERTASKCONTROLLER self +-- @return #table TaskTypes +function PLAYERTASKCONTROLLER:_GetAvailableTaskTypes() + self:I(self.lid.."_GetAvailableTaskTypes") + local tasktypes = {} + self.TaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local type = Task.Type + tasktypes[type] = {} + end + ) + return tasktypes +end + +--- [Internal] Get task per type for the menu +-- @param #PLAYERTASKCONTROLLER self +-- @return #table TasksPerTypes +function PLAYERTASKCONTROLLER:_GetTasksPerType() + self:I(self.lid.."_GetTasksPerType") + local tasktypes = self:_GetAvailableTaskTypes() + + self:I({tasktypes}) + + self.TaskQueue:ForEach( + function (Task) + local task = Task -- Ops.PlayerTask#PLAYERTASK + local type = Task.Type + if task:GetState() ~= "Executing" and not task:IsDone() then + table.insert(tasktypes[type],task) + end + end + ) + + return tasktypes +end + +--- [Internal] Check target queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckTargetQueue() + self:I(self.lid.."_CheckTargetQueue") + if self.TargetQueue:Count() > 0 then + local object = self.TargetQueue:Pull() + local target = TARGET:New(object) + self:_AddTask(target) + end + return self +end + +--- [Internal] Check task queue +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_CheckTaskQueue() + self:I(self.lid.."_CheckTaskQueue") + if self.TaskQueue:Count() > 0 then + -- remove done tasks + local tasks = self.TaskQueue:GetIDStack() + for _id,_entry in pairs(tasks) do + local data = _entry.data -- Ops.PlayerTask#PLAYERTASK + self:I("Looking at Task: "..data.PlayerTaskNr.." Type: "..data.Type.." State: "..data:GetState()) + if data:GetState() == "Done" or data:GetState() == "Stopped" then + local task = self.TaskQueue:ReadByID(_id) -- Ops.PlayerTask#PLAYERTASK + -- TODO: Remove clients from the task + local clientsattask = task.Clients:GetIDStackSorted() + for _,_id in pairs(clientsattask) do + self:I("*****Removing player " .. _id) + self.TasksPerPlayer:PullByID(_id) + end + local task = self.TaskQueue:PullByID(_id) -- Ops.PlayerTask#PLAYERTASK + task = nil + end + end + end + return self +end + +--- [user] Add a target object to the target queue +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Positionable#POSITIONABLE Target The target GROUP, SET_GROUP, UNIT, SET_UNIT, STATIC, AIRBASE or COORDINATE. +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:AddTarget(Target) + self:I(self.lid.."AddTarget") + self.TargetQueue:Push(Target) + return self +end + +--- [Internal] Add a task to the task queue +-- @param #PLAYERTASKCONTROLLER self +-- @param Ops.Target#TARGET Target +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_AddTask(Target) + self:I(self.lid.."_AddTask") + local cat = Target:GetCategory() + local type = AUFTRAG.Type.CAS + + if cat == TARGET.Category.GROUND then + type = AUFTRAG.Type.CAS + -- TODO: debug BAI, CAS, SEAD + local targetobject = Target:GetObject() -- Wrapper.Positionable#POSITIONABLE + if targetobject:IsInstanceOf("UNIT") then + self:I("SEAD Check UNIT") + if targetobject:HasSEAD() then + type = AUFTRAG.Type.SEAD + end + elseif targetobject:IsInstanceOf("GROUP") then + self:I("SEAD Check GROUP") + local attribute = targetobject:GetAttribute() + if attribute == GROUP.Attribute.GROUND_SAM or attribute == GROUP.Attribute.GROUND_AAA then + type = AUFTRAG.Type.SEAD + end + elseif targetobject:IsInstanceOf("SET_GROUP") then + self:I("SEAD Check SET_GROUP") + targetobject:ForEachGroup( + function (group) + local attribute = group:GetAttribute() + if attribute == GROUP.Attribute.GROUND_SAM or attribute == GROUP.Attribute.GROUND_AAA then + type = AUFTRAG.Type.SEAD + end + end + ) + elseif targetobject:IsInstanceOf("SET_UNIT") then + self:I("SEAD Check SET_UNIT") + targetobject:ForEachUnit( + function (unit) + if unit:HasSEAD() then + type = AUFTRAG.Type.SEAD + end + end + ) + end + -- if there are no friendlies nearby ~2km and task isn't SEAD, then it's BAI + local targetcoord = Target:GetCoordinate() + local targetvec2 = targetcoord:GetVec2() + local targetzone = ZONE_RADIUS:New(self.Name,targetvec2,2000) + local coalition = targetobject:GetCoalitionName() or "Blue" + coalition = string.lower(coalition) + self:I("Target coalition is "..tostring(coalition)) + local filtercoalition = "blue" + if coalition == "blue" then filtercoalition = "red" end + local friendlyset = SET_GROUP:New():FilterCategoryGround():FilterCoalitions(filtercoalition):FilterZones({targetzone}):FilterOnce() + if friendlyset:Count() == 0 and type ~= AUFTRAG.Type.SEAD then + type = AUFTRAG.Type.BAI + end + elseif cat == TARGET.Category.NAVAL then + type = AUFTRAG.Type.ANTISHIP + elseif cat == TARGET.Category.AIRCRAFT then + type = AUFTRAG.Type.INTERCEPT + elseif cat == TARGET.Category.AIRBASE then + --TODO: Define Success Criteria, AB hit? Runway blocked, how to determine? change of coalition? + type = AUFTRAG.Type.BOMBRUNWAY + elseif cat == TARGET.Category.COORDINATE or cat == TARGET.Category.ZONE then + --TODO: Define Success Criteria, void of enemies? + type = AUFTRAG.Type.BOMBING + end + + local task = PLAYERTASK:New(type,Target,self.repeatonfailed,self.repeattimes) + task:_SetController(self) + self.TaskQueue:Push(task) + return self +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 +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_JoinTask(Group, Client, Task) + self:I(self.lid.."_JoinTask") + local playername = Client:GetPlayerName() + if self.TasksPerPlayer:HasUniqueID(playername) then + -- Player already has a task + local m=MESSAGE:New("You already have one active task! Complete it first!","10","Info"):ToGroup(Group) + return self + end + Task:AddClient(Client) + local taskstate = Task:GetState() + --self:I(self.lid.."Taskstate = "..taskstate) + if taskstate ~= "Executing" and taskstate ~= "Done" then + Task:__Requested(-1) + Task:__Executing(-2) + local text = string.format("Player %s joined task %d in state %s", playername, Task.PlayerTaskNr, taskstate) + self:I(self.lid..text) + local m=MESSAGE:New(text,"10","Info"):ToAll() + self.TasksPerPlayer:Push(Task,playername) + Task.TaskMenu:Remove() + end + return self +end + +--- [Internal] Show active task info +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_ActiveTaskInfo(Group, Client) + self:I(self.lid.."_ActiveTaskInfo") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + -- TODO: Show multiple + local task = self.TasksPerPlayer:GetIDStack() + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + local taskname = string.format("%s Task ID %02d",task.Type,task.PlayerTaskNr) + local Coordinate = task.Target:GetCoordinate() + local CoordText = Coordinate:ToStringA2G(Client) + local ThreatLevel = task.Target:GetThreatLevelMax() + local targets = task.Target:CountTargets() or 0 + local ThreatGraph = "[" .. string.rep( "■", ThreatLevel ) .. string.rep( "□", 10 - ThreatLevel ) .. "]: "..ThreatLevel + text = string.format("%s\nThreat: %s\nTargets left: %d\nCoord: %s", taskname, ThreatGraph, targets, CoordText) + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Tasking"):ToGroup(Group) + return self +end + +--- [Internal] Mark task on F10 map +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_MarkTask(Group, Client) + self:I(self.lid.."_ActiveTaskInfo") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:MarkTargetOnF10Map() + text = "Task location marked!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Smoke task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_SmokeTask(Group, Client) + self:I(self.lid.."_SmokeTask") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:SmokeTarget() + text = "Task location smoked!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Flare task location +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_FlareTask(Group, Client) + self:I(self.lid.."_FlareTask") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:ReadByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:FlareTarget() + text = "Task location illuminated!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Abort Task +-- @param #PLAYERTASKCONTROLLER self +-- @param Wrapper.Group#GROUP Group +-- @param Wrapper.Client#CLIENT Client +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_AbortTask(Group, Client) + self:I(self.lid.."_FlareTask") + local playername = Client:GetPlayerName() + local text = "" + if self.TasksPerPlayer:HasUniqueID(playername) then + local task = self.TasksPerPlayer:PullByID(playername) -- Ops.PlayerTask#PLAYERTASK + task:ClientAbort(Client) + text = "Task aborted!" + else + text = "No active task!" + end + local m=MESSAGE:New(text,15,"Info"):ToGroup(Group) + return self +end + +--- [Internal] Build client menus +-- @param #PLAYERTASKCONTROLLER self +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:_BuildMenus() + self:I(self.lid.."_BuildMenus") + local clients = self.ClientSet:GetAliveSet() + for _,_client in pairs(clients) do + if _client then + local client = _client -- Wrapper.Client#CLIENT + local group = client:GetGroup() + if group then + local topmenu = MENU_GROUP:New(group,self.Name.." Tasking "..self.Type,nil) + local active = MENU_GROUP:New(group,"Active Task",topmenu) + local info = MENU_GROUP_COMMAND:New(group,"Info",active,self._ActiveTaskInfo,self,group,client) + local mark = MENU_GROUP_COMMAND:New(group,"Mark on map",active,self._MarkTask,self,group,client) + local smoke = MENU_GROUP_COMMAND:New(group,"Smoke",active,self._SmokeTask,self,group,client) + local flare = MENU_GROUP_COMMAND:New(group,"Flare",active,self._FlareTask,self,group,client) + local abort = MENU_GROUP_COMMAND:New(group,"Abort",active,self._AbortTask,self,group,client) + + local join = MENU_GROUP:New(group,"Join Task",topmenu) + + local tasktypes = self:_GetAvailableTaskTypes() + local taskpertype = self:_GetTasksPerType() + + local ttypes = {} + local taskmenu = {} + for _tasktype,_data in pairs(tasktypes) do + ttypes[_tasktype] = MENU_GROUP:New(group,_tasktype,join) + local tasks = taskpertype[_tasktype] or {} + for _,_task in pairs(tasks) do + local text = string.format("TaskNo %03d",_task.PlayerTaskNr) + if self.UseGroupNames then + local name = _task.Target:GetName() + if name ~= "Unknown" then + 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) + taskentry:SetTag(client:GetPlayerName()) + taskmenu[#taskmenu+1] = taskentry + _task.TaskMenu = taskentry + end + end + join:Refresh() + end + end + end + return self +end + +--- [Internal] On after Status call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStatus(From, Event, To) + self:I({From, Event, To}) + self:_CheckTargetQueue() + self:_CheckTaskQueue() + self:_BuildMenus() + + local targetcount = self.TargetQueue:Count() + local taskcount = self.TaskQueue:Count() + local playercount = self.ClientSet:CountAlive() + + if self.verbose then + local text = string.format("New Targets: %02d | Active Tasks: %02d | Active Players: %02d",targetcount,taskcount,playercount) + self:I(text) + end + + if self:GetState() ~= "Stopped" then + self:__Status(-30) + end + return self +end + +--- [Internal] On after task done +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskDone(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskDone") + return self +end + +--- [Internal] On after task cancelled +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskCancelled(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskCancelled") + return self +end + +--- [Internal] On after task success +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskSuccess(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskSuccess") + local taskname = string.format("Task #%d %s Success!", Task.PlayerTaskNr, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after task failed +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskFailed(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."TaskFailed") + local taskname = string.format("Task #%d %s Failed!", Task.PlayerTaskNr, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after task failed, repeat planned +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @param Ops.PlayerTask#PLAYERTASK Task +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterTaskRepeatOnFailed(From, Event, To, Task) + self:I({From, Event, To}) + self:I(self.lid.."RepeatOnFailed") + local taskname = string.format("Task #%d %s Failed! Replanning!", Task.PlayerTaskNr, tostring(Task.Type)) + local m = MESSAGE:New(taskname,15,"Tasking"):ToCoalition(self.Coalition) + return self +end + +--- [Internal] On after Stop call +-- @param #PLAYERTASKCONTROLLER self +-- @param #string From +-- @param #string Event +-- @param #string To +-- @return #PLAYERTASKCONTROLLER self +function PLAYERTASKCONTROLLER:onafterStop(From, Event, To) + self:I({From, Event, To}) + self:I(self.lid.."Stopped.") + return self +end + diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index c21fb28f1..ee493f3ec 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -498,6 +498,8 @@ AIRBASE.MarianaIslands={ -- * AIRBASE.SouthAtlantic.Ushuaia -- * AIRBASE.SouthAtlantic.Ushuaia_Helo_Port -- * AIRBASE.SouthAtlantic.Punta_Arenas +-- * AIRBASE.SouthAtlantic.Pampa_Guanaco +-- * AIRBASE.SouthAtlantic.San_Julian -- --@field MarianaIslands AIRBASE.SouthAtlantic={ @@ -509,6 +511,8 @@ AIRBASE.SouthAtlantic={ ["Ushuaia"]="Ushuaia", ["Ushuaia_Helo_Port"]="Ushuaia Helo Port", ["Punta_Arenas"]="Punta Arenas", + ["Pampa_Guanaco"]="Pampa Guanaco", + ["San_Julian"]="San Julian", } --- AIRBASE.ParkingSpot ".Coordinate, ".TerminalID", ".TerminalType", ".TOAC", ".Free", ".TerminalID0", ".DistToRwy". diff --git a/Moose Development/Moose/Wrapper/Positionable.lua b/Moose Development/Moose/Wrapper/Positionable.lua index 203c8042f..93284a621 100644 --- a/Moose Development/Moose/Wrapper/Positionable.lua +++ b/Moose Development/Moose/Wrapper/Positionable.lua @@ -532,7 +532,7 @@ function POSITIONABLE:GetBoundingRadius(mindist) return nil end ---- Returns the altitude of the POSITIONABLE. +--- Returns the altitude above sea level of the POSITIONABLE. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Distance The altitude of the POSITIONABLE. -- @return #nil The POSITIONABLE is not existing or alive. @@ -775,7 +775,7 @@ function POSITIONABLE:GetRelativeVelocity(positionable) end ---- Returns the POSITIONABLE height in meters. +--- Returns the POSITIONABLE height above sea level in meters. -- @param Wrapper.Positionable#POSITIONABLE self -- @return DCS#Vec3 The height of the positionable. -- @return #nil The POSITIONABLE is not existing or alive. diff --git a/Moose Development/Moose/Wrapper/Unit.lua b/Moose Development/Moose/Wrapper/Unit.lua index 5b2f34241..1eea61355 100644 --- a/Moose Development/Moose/Wrapper/Unit.lua +++ b/Moose Development/Moose/Wrapper/Unit.lua @@ -179,8 +179,28 @@ function UNIT:GetDCSObject() return nil end +--- Returns the unit altitude above sea level in meters. +-- @param Wrapper.Unit#UNIT self +-- @param #boolean FromGround Measure from the ground or from sea level (ASL). Provide **true** for measuring from the ground (AGL). **false** or **nil** if you measure from sea level. +-- @return #number The height of the group or nil if is not existing or alive. +function UNIT:GetAltitude(FromGround) + + local DCSUnit = Unit.getByName( self.UnitName ) + if DCSUnit then + local altitude = 0 + local point = DCSUnit.getPoint() --DCS#Vec3 + altitude = point.y + if FromGround then + local land = land.getHeight( { x = point.x, y = point.z } ) or 0 + altitude = altitude - land + end + return altitude + end + return nil + +end --- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group. -- diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 930f2cb06..4eea18a66 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -99,6 +99,7 @@ Ops/CTLD.lua Ops/Awacs.lua Ops/Operation.lua Ops/FlightControl.lua +Ops/PlayerTask.lua AI/AI_Balancer.lua AI/AI_Air.lua