From 8ce1f5884f3053d1adb5aba03e34828a9f225952 Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Fri, 29 Jul 2022 12:24:36 +0200 Subject: [PATCH] PlayerTask Beta --- Moose Development/Moose/Functional/RAT.lua | 12 +- Moose Development/Moose/Modules.lua | 1 + Moose Development/Moose/Ops/PlayerTask.lua | 967 +++++++++++++++++++++ 3 files changed, 977 insertions(+), 3 deletions(-) create mode 100644 Moose Development/Moose/Ops/PlayerTask.lua 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..55227ad58 --- /dev/null +++ b/Moose Development/Moose/Ops/PlayerTask.lua @@ -0,0 +1,967 @@ +--- **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 Table of Wrapper.Client#CLIENT planes executing this task +-- +-- @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 = {}, + } + +--- PLAYERTASK class version. +-- @field #string version +PLAYERTASK.version="0.0.3" + +--- 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 = {} + + 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 + +--- (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:GetName() + 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:GetName() + if self.Clients:HasUniqueID(name) then + self.Clients:PullByID(name) + self:__ClientRemoved(-2,Client) + if self.Clients:Count() == 0 then + self:__Planned(-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") + local name = Client:GetName() + 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 + self:__Planned(-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}) + 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}) + 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.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 + self:__Planned(-1) + return self + else + if self.TargetMarker then + self.TargetMarker:Remove() + 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 #PLAYERTASKCONTROLLER +PLAYERTASKCONTROLLER = { + ClassName = "PLAYERTASKCONTROLLER", + verbose = true, + lid = nil, + TargetQueue = nil, + ClientSet = nil, + } + +--- +-- @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.1" + +--- 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 + + 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("*", "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 + +--- [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:PullByID(_id) + -- TODO: Remove clients from the task + 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? 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) + + 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() + 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: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) + 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 + 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 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 +